diff --git a/pyomo/solvers/plugins/solvers/cplex_direct.py b/pyomo/solvers/plugins/solvers/cplex_direct.py index eb64dee63f8..5ab58a832da 100644 --- a/pyomo/solvers/plugins/solvers/cplex_direct.py +++ b/pyomo/solvers/plugins/solvers/cplex_direct.py @@ -37,13 +37,22 @@ class DegreeError(ValueError): class _CplexExpr(object): - def __init__(self): - self.variables = [] - self.coefficients = [] - self.offset = 0 - self.q_variables1 = [] - self.q_variables2 = [] - self.q_coefficients = [] + def __init__( + self, + variables, + coefficients, + offset=None, + q_variables1=None, + q_variables2=None, + q_coefficients=None, + ): + self.variables = variables + self.coefficients = coefficients + self.offset = offset or 0.0 + self.q_variables1 = q_variables1 or [] + self.q_variables2 = q_variables2 or [] + self.q_coefficients = [float(coef) for coef in q_coefficients or []] + def _is_numeric(x): try: @@ -53,6 +62,52 @@ def _is_numeric(x): return True +class _VariableData(object): + def __init__(self, solver_model): + self._solver_model = solver_model + self.lb = [] + self.ub = [] + self.types = [] + self.names = [] + + def add(self, lb, ub, type_, name): + self.lb.append(lb) + self.ub.append(ub) + self.types.append(type_) + self.names.append(name) + + def store_in_cplex(self): + self._solver_model.variables.add( + lb=self.lb, ub=self.ub, types=self.types, names=self.names + ) + + +class _LinearConstraintData(object): + def __init__(self, solver_model): + self._solver_model = solver_model + self.lin_expr = [] + self.senses = [] + self.rhs = [] + self.range_values = [] + self.names = [] + + def add(self, cplex_expr, sense, rhs, range_values, name): + self.lin_expr.append([cplex_expr.variables, cplex_expr.coefficients]) + self.senses.append(sense) + self.rhs.append(rhs) + self.range_values.append(range_values) + self.names.append(name) + + def store_in_cplex(self): + self._solver_model.linear_constraints.add( + lin_expr=self.lin_expr, + senses=self.senses, + rhs=self.rhs, + range_values=self.range_values, + names=self.names, + ) + + @SolverFactory.register('cplex_direct', doc='Direct python interface to CPLEX') class CPLEXDirect(DirectSolver): @@ -203,29 +258,36 @@ def _process_stream(arg): return Bunch(rc=None, log=None) def _get_expr_from_pyomo_repn(self, repn, max_degree=2): - referenced_vars = ComponentSet() - degree = repn.polynomial_degree() - if (degree is None) or (degree > max_degree): - raise DegreeError('CPLEXDirect does not support expressions of degree {0}.'.format(degree)) + if degree is None or degree > max_degree: + raise DegreeError( + "CPLEXDirect does not support expressions of degree {0}.".format(degree) + ) - new_expr = _CplexExpr() - if len(repn.linear_vars) > 0: - referenced_vars.update(repn.linear_vars) - new_expr.variables.extend(self._pyomo_var_to_ndx_map[i] for i in repn.linear_vars) - new_expr.coefficients.extend(repn.linear_coefs) + referenced_vars = ComponentSet(repn.linear_vars) + q_coefficients = [] + q_variables1 = [] + q_variables2 = [] for i, v in enumerate(repn.quadratic_vars): x, y = v - new_expr.q_coefficients.append(repn.quadratic_coefs[i]) - new_expr.q_variables1.append(self._pyomo_var_to_ndx_map[x]) - new_expr.q_variables2.append(self._pyomo_var_to_ndx_map[y]) + q_coefficients.append(repn.quadratic_coefs[i]) + q_variables1.append(self._pyomo_var_to_ndx_map[x]) + q_variables2.append(self._pyomo_var_to_ndx_map[y]) referenced_vars.add(x) referenced_vars.add(y) - new_expr.offset = repn.constant - - return new_expr, referenced_vars + return ( + _CplexExpr( + variables=[self._pyomo_var_to_ndx_map[var] for var in repn.linear_vars], + coefficients=repn.linear_coefs, + offset=repn.constant, + q_variables1=q_variables1, + q_variables2=q_variables2, + q_coefficients=q_coefficients, + ), + referenced_vars, + ) def _get_expr_from_pyomo_expr(self, expr, max_degree=2): if max_degree == 2: @@ -242,7 +304,7 @@ def _get_expr_from_pyomo_expr(self, expr, max_degree=2): return cplex_expr, referenced_vars - def _add_var(self, var): + def _add_var(self, var, var_data=None): varname = self._symbol_map.getSymbol(var, self._labeler) vtype = self._cplex_vtype_from_var(var) if var.has_lb(): @@ -254,7 +316,16 @@ def _add_var(self, var): else: ub = self._cplex.infinity - self._solver_model.variables.add(lb=[lb], ub=[ub], types=[vtype], names=[varname]) + if var.is_fixed(): + lb = value(var) + ub = value(var) + + cplex_var_data = ( + _VariableData(self._solver_model) if var_data is None else var_data + ) + cplex_var_data.add(lb=lb, ub=ub, type_=vtype, name=varname) + if var_data is None: + cplex_var_data.store_in_cplex() self._pyomo_var_to_solver_var_map[var] = varname self._solver_var_to_pyomo_var_map[varname] = var @@ -262,10 +333,6 @@ def _add_var(self, var): self._ndx_count += 1 self._referenced_variables[var] = 0 - if var.is_fixed(): - self._solver_model.variables.set_lower_bounds(varname, var.value) - self._solver_model.variables.set_upper_bounds(varname, var.value) - def _set_instance(self, model, kwds={}): self._pyomo_var_to_ndx_map = ComponentMap() self._ndx_count = 0 @@ -297,7 +364,51 @@ def _set_instance(self, model, kwds={}): "by overwriting its bounds in the CPLEX instance." % (var.name, self._pyomo_model.name,)) - def _add_constraint(self, con): + def _add_block(self, block): + var_data = _VariableData(self._solver_model) + for var in block.component_data_objects( + ctype=pyomo.core.base.var.Var, descend_into=True, active=True, sort=True + ): + self._add_var(var, var_data) + var_data.store_in_cplex() + + lin_con_data = _LinearConstraintData(self._solver_model) + for sub_block in block.block_data_objects(descend_into=True, active=True): + for con in sub_block.component_data_objects( + ctype=pyomo.core.base.constraint.Constraint, + descend_into=False, + active=True, + sort=True, + ): + if not con.has_lb() and not con.has_ub(): + assert not con.equality + continue # non-binding, so skip + + self._add_constraint(con, lin_con_data) + + for con in sub_block.component_data_objects( + ctype=pyomo.core.base.sos.SOSConstraint, + descend_into=False, + active=True, + sort=True, + ): + self._add_sos_constraint(con) + + obj_counter = 0 + for obj in sub_block.component_data_objects( + ctype=pyomo.core.base.objective.Objective, + descend_into=False, + active=True, + ): + obj_counter += 1 + if obj_counter > 1: + raise ValueError( + "Solver interface does not support multiple objectives." + ) + self._set_objective(obj) + lin_con_data.store_in_cplex() + + def _add_constraint(self, con, lin_con_data=None): if not con.active: return None @@ -309,55 +420,57 @@ def _add_constraint(self, con): if con._linear_canonical_form: cplex_expr, referenced_vars = self._get_expr_from_pyomo_repn( - con.canonical_form(), - self._max_constraint_degree) + con.canonical_form(), self._max_constraint_degree + ) else: cplex_expr, referenced_vars = self._get_expr_from_pyomo_expr( - con.body, - self._max_constraint_degree) - - if con.has_lb(): - 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)) + con.body, self._max_constraint_degree + ) + + if con.has_lb() and not is_fixed(con.lower): + raise ValueError( + "Lower bound of constraint {0} is not constant.".format(con) + ) + if con.has_ub() and not is_fixed(con.upper): + raise ValueError( + "Upper bound of constraint {0} is not constant.".format(con) + ) + + range_ = 0.0 if con.equality: - my_sense = 'E' - my_rhs = [value(con.lower) - cplex_expr.offset] - my_range = [] + sense = "E" + rhs = value(con.lower) - cplex_expr.offset elif con.has_lb() and con.has_ub(): - my_sense = 'R' + sense = "R" lb = value(con.lower) ub = value(con.upper) - my_rhs = [ub - cplex_expr.offset] - my_range = [lb - ub] + rhs = ub - cplex_expr.offset + range_ = lb - ub self._range_constraints.add(con) elif con.has_lb(): - my_sense = 'G' - my_rhs = [value(con.lower) - cplex_expr.offset] - my_range = [] + sense = "G" + rhs = value(con.lower) - cplex_expr.offset elif con.has_ub(): - my_sense = 'L' - my_rhs = [value(con.upper) - cplex_expr.offset] - my_range = [] + sense = "L" + rhs = value(con.upper) - cplex_expr.offset else: - raise ValueError("Constraint does not have a lower " - "or an upper bound: {0} \n".format(con)) + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) if len(cplex_expr.q_coefficients) == 0: - self._solver_model.linear_constraints.add( - lin_expr=[[cplex_expr.variables, - cplex_expr.coefficients]], - senses=my_sense, - rhs=my_rhs, - range_values=my_range, - names=[conname]) + cplex_lin_con_data = ( + _LinearConstraintData(self._solver_model) + if lin_con_data is None + else lin_con_data + ) + cplex_lin_con_data.add(cplex_expr, sense, rhs, range_, conname) + if lin_con_data is None: + cplex_lin_con_data.store_in_cplex() else: - if my_sense == 'R': + if sense == 'R': raise ValueError("The CPLEXDirect interface does not " "support quadratic range constraints: " "{0}".format(con)) @@ -367,8 +480,8 @@ def _add_constraint(self, con): quad_expr=[cplex_expr.q_variables1, cplex_expr.q_variables2, cplex_expr.q_coefficients], - sense=my_sense, - rhs=my_rhs[0], + sense=sense, + rhs=rhs, name=conname) for var in referenced_vars: @@ -436,9 +549,6 @@ def _set_objective(self, obj): self._vars_referenced_by_obj = ComponentSet() self._objective = None - self._solver_model.objective.set_linear([(i, 0.0) for i in range(len(self._pyomo_var_to_solver_var_map.values()))]) - self._solver_model.objective.set_quadratic([[[0], [0]] for i in self._pyomo_var_to_solver_var_map.keys()]) - if obj.active is False: raise ValueError('Cannot add inactive objective to solver.') @@ -459,12 +569,33 @@ def _set_objective(self, obj): self._solver_model.objective.set_sense(sense) if hasattr(self._solver_model.objective, 'set_offset'): self._solver_model.objective.set_offset(cplex_expr.offset) - if len(cplex_expr.coefficients) != 0: - self._solver_model.objective.set_linear(list(zip(cplex_expr.variables, cplex_expr.coefficients))) - if len(cplex_expr.q_coefficients) != 0: - self._solver_model.objective.set_quadratic_coefficients(list(zip(cplex_expr.q_variables1, - cplex_expr.q_variables2, - cplex_expr.q_coefficients))) + + linear_objective_already_exists = any(self._solver_model.objective.get_linear()) + quadratic_objective_already_exists = self._solver_model.objective.get_num_quadratic_nonzeros() + + contains_linear_terms = any(cplex_expr.coefficients) + contains_quadratic_terms = any(cplex_expr.q_coefficients) + num_cols = len(self._pyomo_var_to_solver_var_map) + + if linear_objective_already_exists or contains_linear_terms: + self._solver_model.objective.set_linear([(i, 0.0) for i in range(num_cols)]) + + if contains_linear_terms: + self._solver_model.objective.set_linear(list(zip(cplex_expr.variables, cplex_expr.coefficients))) + + if quadratic_objective_already_exists or contains_quadratic_terms: + self._solver_model.objective.set_quadratic([0.0] * num_cols) + + if contains_quadratic_terms: + self._solver_model.objective.set_quadratic_coefficients( + list( + zip( + cplex_expr.q_variables1, + cplex_expr.q_variables2, + cplex_expr.q_coefficients + ) + ) + ) self._objective = obj self._vars_referenced_by_obj = referenced_vars @@ -642,13 +773,13 @@ def _postsolve(self): soln_constraints = soln.constraint var_names = self._solver_model.variables.get_names() - var_names = list(set(var_names).intersection(set(self._pyomo_var_to_solver_var_map.values()))) - var_vals = self._solver_model.solution.get_values(var_names) - for i, name in enumerate(var_names): + assert set(var_names) == set(self._pyomo_var_to_solver_var_map.values()) + var_vals = self._solver_model.solution.get_values() + for name, val in zip(var_names, var_vals): pyomo_var = self._solver_var_to_pyomo_var_map[name] if self._referenced_variables[pyomo_var] > 0: pyomo_var.stale = False - soln_variables[name] = {"Value":var_vals[i]} + soln_variables[name] = {"Value": val} if extract_reduced_costs: reduced_costs = self._solver_model.solution.get_reduced_costs(var_names) @@ -736,18 +867,18 @@ def _warm_start(self): self._solver_model.MIP_starts.effort_level.auto) def _load_vars(self, vars_to_load=None): - var_map = self._pyomo_var_to_solver_var_map - ref_vars = self._referenced_variables + var_map = self._pyomo_var_to_ndx_map if vars_to_load is None: + vals = self._solver_model.solution.get_values() vars_to_load = var_map.keys() + else: + cplex_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] + vals = self._solver_model.solution.get_values(cplex_vars_to_load) - cplex_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] - vals = self._solver_model.solution.get_values(cplex_vars_to_load) - - for i, pyomo_var in enumerate(vars_to_load): - if ref_vars[pyomo_var] > 0: + for pyomo_var, val in zip(vars_to_load, vals): + if self._referenced_variables[pyomo_var] > 0: pyomo_var.stale = False - pyomo_var.value = vals[i] + pyomo_var.value = val def _load_rc(self, vars_to_load=None): if not hasattr(self._pyomo_model, 'rc'): diff --git a/pyomo/solvers/tests/checks/test_CPLEXDirect.py b/pyomo/solvers/tests/checks/test_CPLEXDirect.py index df4f0dca4e0..5d087f5e81c 100644 --- a/pyomo/solvers/tests/checks/test_CPLEXDirect.py +++ b/pyomo/solvers/tests/checks/test_CPLEXDirect.py @@ -17,6 +17,10 @@ from pyomo.environ import * from pyomo.opt import * +from pyomo.solvers.plugins.solvers.cplex_direct import (_CplexExpr, + _LinearConstraintData, + _VariableData) + try: import cplex cplexpy_available = True @@ -292,6 +296,7 @@ def test_dettime_limit_mip(self): @unittest.skipIf(not cplexpy_available, "The 'cplex' python bindings are not available") class TestIsFixedCallCount(unittest.TestCase): """ Tests for PR#1402 (669e7b2b) """ + def setup(self, skip_trivial_constraints): m = ConcreteModel() m.x = Var() @@ -371,5 +376,359 @@ def test_dont_skip_trivial_and_call_count_for_unfixed_con_is_one(self): self.assertEqual(mock_is_fixed.call_count, 1) +@unittest.skipIf(not cplexpy_available, "The 'cplex' python bindings are not available") +class TestDataContainers(unittest.TestCase): + def test_variable_data(self): + solver_model = cplex.Cplex() + var_data = _VariableData(solver_model) + var_data.add(lb=0, ub=1, type_=solver_model.variables.type.binary, name="var1") + var_data.add( + lb=0, ub=10, type_=solver_model.variables.type.integer, name="var2" + ) + var_data.add( + lb=-cplex.infinity, + ub=cplex.infinity, + type_=solver_model.variables.type.continuous, + name="var3", + ) + self.assertEqual(solver_model.variables.get_num(), 0) + var_data.store_in_cplex() + self.assertEqual(solver_model.variables.get_num(), 3) + + def test_constraint_data(self): + solver_model = cplex.Cplex() + + solver_model.variables.add( + lb=[-cplex.infinity, -cplex.infinity, -cplex.infinity], + ub=[cplex.infinity, cplex.infinity, cplex.infinity], + types=[ + solver_model.variables.type.continuous, + solver_model.variables.type.continuous, + solver_model.variables.type.continuous, + ], + names=["var1", "var2", "var3"], + ) + con_data = _LinearConstraintData(solver_model) + con_data.add( + cplex_expr=_CplexExpr(variables=[0, 1], coefficients=[10, 100]), + sense="L", + rhs=0, + range_values=0, + name="c1", + ) + con_data.add( + cplex_expr=_CplexExpr(variables=[0], coefficients=[-30]), + sense="G", + rhs=1, + range_values=0, + name="c2", + ) + con_data.add( + cplex_expr=_CplexExpr(variables=[1], coefficients=[80]), + sense="E", + rhs=2, + range_values=0, + name="c3", + ) + con_data.add( + cplex_expr=_CplexExpr(variables=[2], coefficients=[50]), + sense="R", + rhs=3, + range_values=10, + name="c4", + ) + + self.assertEqual(solver_model.linear_constraints.get_num(), 0) + con_data.store_in_cplex() + self.assertEqual(solver_model.linear_constraints.get_num(), 4) + + +@unittest.skipIf(not unittest.mock_available, "'mock' is not available") +@unittest.skipIf(not cplexpy_available, "The 'cplex' python bindings are not available") +class TestAddVar(unittest.TestCase): + def test_add_single_variable(self): + """ Test that the variable is added correctly to `solver_model`. """ + model = ConcreteModel() + + opt = SolverFactory("cplex", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.variables.get_num(), 0) + self.assertEqual(opt._solver_model.variables.get_num_binary(), 0) + + model.X = Var(within=Binary) + + var_interface = opt._solver_model.variables + with unittest.mock.patch.object( + var_interface, "add", wraps=var_interface.add + ) as wrapped_add_call, unittest.mock.patch.object( + var_interface, "set_lower_bounds", wraps=var_interface.set_lower_bounds + ) as wrapped_lb_call, unittest.mock.patch.object( + var_interface, "set_upper_bounds", wraps=var_interface.set_upper_bounds + ) as wrapped_ub_call: + opt._add_var(model.X) + + self.assertEqual(wrapped_add_call.call_count, 1) + self.assertEqual( + wrapped_add_call.call_args, + ({"lb": [0], "names": ["x1"], "types": ["B"], "ub": [1]},), + ) + + self.assertFalse(wrapped_lb_call.called) + self.assertFalse(wrapped_ub_call.called) + + self.assertEqual(opt._solver_model.variables.get_num(), 1) + self.assertEqual(opt._solver_model.variables.get_num_binary(), 1) + + def test_add_block_containing_single_variable(self): + """ Test that the variable is added correctly to `solver_model`. """ + model = ConcreteModel() + + opt = SolverFactory("cplex", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.variables.get_num(), 0) + self.assertEqual(opt._solver_model.variables.get_num_binary(), 0) + + model.X = Var(within=Binary) + + with unittest.mock.patch.object( + opt._solver_model.variables, "add", wraps=opt._solver_model.variables.add + ) as wrapped_add_call: + opt._add_block(model) + + self.assertEqual(wrapped_add_call.call_count, 1) + self.assertEqual( + wrapped_add_call.call_args, + ({"lb": [0], "names": ["x1"], "types": ["B"], "ub": [1]},), + ) + + self.assertEqual(opt._solver_model.variables.get_num(), 1) + self.assertEqual(opt._solver_model.variables.get_num_binary(), 1) + + def test_add_block_containing_multiple_variables(self): + """ Test that: + - The variable is added correctly to `solver_model` + - The CPLEX `variables` interface is called only once + - Fixed variable bounds are set correctly + """ + model = ConcreteModel() + + opt = SolverFactory("cplex", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.variables.get_num(), 0) + + model.X1 = Var(within=Binary) + model.X2 = Var(within=NonNegativeReals) + model.X3 = Var(within=NonNegativeIntegers) + + model.X3.fix(5) + + with unittest.mock.patch.object( + opt._solver_model.variables, "add", wraps=opt._solver_model.variables.add + ) as wrapped_add_call: + opt._add_block(model) + + self.assertEqual(wrapped_add_call.call_count, 1) + self.assertEqual( + wrapped_add_call.call_args, + ( + { + "lb": [0, 0, 5], + "names": ["x1", "x2", "x3"], + "types": ["B", "C", "I"], + "ub": [1, cplex.infinity, 5], + }, + ), + ) + + self.assertEqual(opt._solver_model.variables.get_num(), 3) + + +@unittest.skipIf(not unittest.mock_available, "'mock' is not available") +@unittest.skipIf(not cplexpy_available, "The 'cplex' python bindings are not available") +class TestAddCon(unittest.TestCase): + def test_add_single_constraint(self): + model = ConcreteModel() + model.X = Var(within=Binary) + + opt = SolverFactory("cplex", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.linear_constraints.get_num(), 0) + + model.C = Constraint(expr=model.X == 1) + + con_interface = opt._solver_model.linear_constraints + with unittest.mock.patch.object( + con_interface, "add", wraps=con_interface.add + ) as wrapped_add_call: + opt._add_constraint(model.C) + + self.assertEqual(wrapped_add_call.call_count, 1) + self.assertEqual( + wrapped_add_call.call_args, + ( + { + "lin_expr": [[[0], (1,)]], + "names": ["x2"], + "range_values": [0.0], + "rhs": [1.0], + "senses": ["E"], + }, + ), + ) + + self.assertEqual(opt._solver_model.linear_constraints.get_num(), 1) + + def test_add_block_containing_single_constraint(self): + model = ConcreteModel() + model.X = Var(within=Binary) + + opt = SolverFactory("cplex", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.linear_constraints.get_num(), 0) + + model.B = Block() + model.B.C = Constraint(expr=model.X == 1) + + con_interface = opt._solver_model.linear_constraints + with unittest.mock.patch.object( + con_interface, "add", wraps=con_interface.add + ) as wrapped_add_call: + opt._add_block(model.B) + + self.assertEqual(wrapped_add_call.call_count, 1) + self.assertEqual( + wrapped_add_call.call_args, + ( + { + "lin_expr": [[[0], (1,)]], + "names": ["x2"], + "range_values": [0.0], + "rhs": [1.0], + "senses": ["E"], + }, + ), + ) + + self.assertEqual(opt._solver_model.linear_constraints.get_num(), 1) + + def test_add_block_containing_multiple_constraints(self): + model = ConcreteModel() + model.X = Var(within=Binary) + + opt = SolverFactory("cplex", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.linear_constraints.get_num(), 0) + + model.B = Block() + model.B.C1 = Constraint(expr=model.X == 1) + model.B.C2 = Constraint(expr=model.X <= 1) + model.B.C3 = Constraint(expr=model.X >= 1) + + con_interface = opt._solver_model.linear_constraints + with unittest.mock.patch.object( + con_interface, "add", wraps=con_interface.add + ) as wrapped_add_call: + opt._add_block(model.B) + + self.assertEqual(wrapped_add_call.call_count, 1) + self.assertEqual( + wrapped_add_call.call_args, + ( + { + "lin_expr": [[[0], (1,)], [[0], (1,)], [[0], (1,)]], + "names": ["x2", "x3", "x4"], + "range_values": [0.0, 0.0, 0.0], + "rhs": [1.0, 1.0, 1.0], + "senses": ["E", "L", "G"], + }, + ), + ) + + self.assertEqual(opt._solver_model.linear_constraints.get_num(), 3) + + +@unittest.skipIf(not unittest.mock_available, "'mock' is not available") +@unittest.skipIf(not cplexpy_available, "The 'cplex' python bindings are not available") +class TestLoadVars(unittest.TestCase): + def setUp(self): + opt = SolverFactory("cplex", solver_io="python") + model = ConcreteModel() + model.X = Var(within=NonNegativeReals, initialize=0) + model.Y = Var(within=NonNegativeReals, initialize=0) + + model.C1 = Constraint(expr=2 * model.X + model.Y >= 8) + model.C2 = Constraint(expr=model.X + 3 * model.Y >= 6) + + model.O = Objective(expr=model.X + model.Y) + + opt.solve(model, load_solutions=False, save_results=False) + + self._model = model + self._opt = opt + + def test_all_vars_are_loaded(self): + self.assertTrue(self._model.X.stale) + self.assertTrue(self._model.Y.stale) + self.assertEqual(value(self._model.X), 0) + self.assertEqual(value(self._model.Y), 0) + + with unittest.mock.patch.object( + self._opt._solver_model.solution, + "get_values", + wraps=self._opt._solver_model.solution.get_values, + ) as wrapped_values_call: + self._opt.load_vars() + + self.assertEqual(wrapped_values_call.call_count, 1) + self.assertEqual(wrapped_values_call.call_args, tuple()) + + self.assertFalse(self._model.X.stale) + self.assertFalse(self._model.Y.stale) + self.assertAlmostEqual(value(self._model.X), 3.6) + self.assertAlmostEqual(value(self._model.Y), 0.8) + + def test_only_specified_vars_are_loaded(self): + self.assertTrue(self._model.X.stale) + self.assertTrue(self._model.Y.stale) + self.assertEqual(value(self._model.X), 0) + self.assertEqual(value(self._model.Y), 0) + + with unittest.mock.patch.object( + self._opt._solver_model.solution, + "get_values", + wraps=self._opt._solver_model.solution.get_values, + ) as wrapped_values_call: + self._opt.load_vars([self._model.X]) + + self.assertEqual(wrapped_values_call.call_count, 1) + self.assertEqual(wrapped_values_call.call_args, (([0],), {})) + + self.assertFalse(self._model.X.stale) + self.assertTrue(self._model.Y.stale) + self.assertAlmostEqual(value(self._model.X), 3.6) + self.assertEqual(value(self._model.Y), 0) + + with unittest.mock.patch.object( + self._opt._solver_model.solution, + "get_values", + wraps=self._opt._solver_model.solution.get_values, + ) as wrapped_values_call: + self._opt.load_vars([self._model.Y]) + + self.assertEqual(wrapped_values_call.call_count, 1) + self.assertEqual(wrapped_values_call.call_args, (([1],), {})) + + self.assertFalse(self._model.X.stale) + self.assertFalse(self._model.Y.stale) + self.assertAlmostEqual(value(self._model.X), 3.6) + self.assertAlmostEqual(value(self._model.Y), 0.8) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/solvers/tests/checks/test_CPLEXPersistent.py b/pyomo/solvers/tests/checks/test_CPLEXPersistent.py new file mode 100644 index 00000000000..51ff1ab7e43 --- /dev/null +++ b/pyomo/solvers/tests/checks/test_CPLEXPersistent.py @@ -0,0 +1,35 @@ +import pyutilib.th as unittest + +from pyomo.environ import * +from pyomo.opt import * + +try: + import cplex + + cplexpy_available = True +except ImportError: + cplexpy_available = False + + +@unittest.skipIf(not cplexpy_available, "The 'cplex' python bindings are not available") +class TestQuadraticObjective(unittest.TestCase): + def test_quadratic_objective_is_set(self): + model = ConcreteModel() + model.X = Var(bounds=(-2, 2)) + model.Y = Var(bounds=(-2, 2)) + model.O = Objective(expr=model.X ** 2 + model.Y ** 2) + model.C1 = Constraint(expr=model.Y >= 2 * model.X - 1) + model.C2 = Constraint(expr=model.Y >= -model.X + 2) + opt = SolverFactory("cplex_persistent") + opt.set_instance(model) + opt.solve() + + self.assertAlmostEqual(model.X.value, 1, places=3) + self.assertAlmostEqual(model.Y.value, 1, places=3) + + del model.O + model.O = Objective(expr=model.X ** 2) + opt.set_objective(model.O) + opt.solve() + self.assertAlmostEqual(model.X.value, 0, places=3) + self.assertAlmostEqual(model.Y.value, 2, places=3)