Skip to content

Commit

Permalink
Merge branch 'main' into fix-transformed-gdp-pickle
Browse files Browse the repository at this point in the history
  • Loading branch information
emma58 committed Nov 1, 2022
2 parents a31fcf6 + d8de0d0 commit 9f6c7be
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 113 deletions.
8 changes: 4 additions & 4 deletions pyomo/common/getGSL.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
# These URLs were retrieved from
# https://ampl.com/resources/extended-function-library/
urlmap = {
'linux': 'https://old.ampl.com/dl/open/amplgsl/amplgsl-linux%s.zip',
'windows': 'https://old.ampl.com/dl/open/amplgsl/amplgsl-win%s.zip',
'cygwin': 'https://old.ampl.com/dl/open/amplgsl/amplgsl-win%s.zip',
'darwin': 'https://old.ampl.com/dl/open/amplgsl/amplgsl-osx.zip'
'linux': 'https://ampl.com/dl/open/amplgsl/amplgsl-linux%s.zip',
'windows': 'https://ampl.com/dl/open/amplgsl/amplgsl-win%s.zip',
'cygwin': 'https://ampl.com/dl/open/amplgsl/amplgsl-win%s.zip',
'darwin': 'https://ampl.com/dl/open/amplgsl/amplgsl-osx.zip'
}

def find_GSL():
Expand Down
106 changes: 98 additions & 8 deletions pyomo/core/expr/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
NPV_AbsExpression, NumericValue,
RangedExpression, InequalityExpression, EqualityExpression,
)
from .template_expr import GetItemExpression
from typing import List
from pyomo.common.errors import PyomoException
from pyomo.common.formatting import tostr
from pyomo.common.numeric_types import native_types


def handle_linear_expression(node: LinearExpression, pn: List):
Expand Down Expand Up @@ -76,6 +79,7 @@ def handle_external_function_expression(node: ExternalFunctionExpression, pn: Li
handler[RangedExpression] = handle_expression
handler[InequalityExpression] = handle_expression
handler[EqualityExpression] = handle_expression
handler[GetItemExpression] = handle_expression


class PrefixVisitor(StreamBasedExpressionVisitor):
Expand Down Expand Up @@ -157,9 +161,7 @@ def convert_expression_to_prefix_notation(expr, include_named_exprs=True):


def compare_expressions(expr1, expr2, include_named_exprs=True):
"""
Returns True if 2 expression trees are identical. Returns False
otherwise.
"""Returns True if 2 expression trees are identical, False otherwise.
Parameters
----------
Expand All @@ -168,20 +170,108 @@ def compare_expressions(expr1, expr2, include_named_exprs=True):
expr2: NumericValue
A Pyomo Var, Param, or expression
include_named_exprs: bool
If False, then named expressions will be ignored. In other words, this function
will return True if one expression has a named expression and the other does not
as long as the rest of the expression trees are identical.
If False, then named expressions will be ignored. In other
words, this function will return True if one expression has a
named expression and the other does not as long as the rest of
the expression trees are identical.
Returns
-------
res: bool
A bool indicating whether or not the expressions are identical.
"""
pn1 = convert_expression_to_prefix_notation(expr1, include_named_exprs=include_named_exprs)
pn2 = convert_expression_to_prefix_notation(expr2, include_named_exprs=include_named_exprs)
pn1 = convert_expression_to_prefix_notation(
expr1, include_named_exprs=include_named_exprs)
pn2 = convert_expression_to_prefix_notation(
expr2, include_named_exprs=include_named_exprs)
try:
res = pn1 == pn2
except PyomoException:
res = False
return res


def assertExpressionsEqual(test, a, b, include_named_exprs=True):
"""unittest-based assertion for comparing expressions
This converts the expressions `a` and `b` into prefix notation and
then compares the resulting lists.
Parameters
----------
test: unittest.TestCase
The unittest `TestCase` class that is performing the test.
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
"""
prefix_a = convert_expression_to_prefix_notation(a, include_named_exprs)
prefix_b = convert_expression_to_prefix_notation(b, include_named_exprs)
try:
test.assertEqual(len(prefix_a), len(prefix_b))
for _a, _b in zip(prefix_a, prefix_b):
test.assertIs(_a.__class__, _b.__class__)
test.assertEqual(_a, _b)
except (PyomoException, AssertionError):
test.fail(f"Expressions not equal:\n\t"
f"{tostr(prefix_a)}\n\t!=\n\t{tostr(prefix_b)}")


def assertExpressionsStructurallyEqual(test, a, b, include_named_exprs=True):
"""unittest-based assertion for comparing expressions
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
----------
test: unittest.TestCase
The unittest `TestCase` class that is performing the test.
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
"""
prefix_a = convert_expression_to_prefix_notation(a, include_named_exprs)
prefix_b = convert_expression_to_prefix_notation(b, include_named_exprs)
# Convert leaf nodes and operators to their string equivalents
for prefix in (prefix_a, prefix_b):
for i, v in enumerate(prefix):
if type(v) in native_types:
continue
if type(v) is tuple:
# This is an expression node. Most expression nodes are
# 2-tuples (node type, nargs), but some are 3-tuples
# with supplemental data. The biggest problem is
# external functions, where the third element is the
# external function. We need to convert that to a
# string to support "structural" comparisons.
if len(v) == 3:
prefix[i] = v[:2] + (str(v[2]),)
continue
# This should be a leaf node (Var, mutable Param, etc.).
# Convert to string to support "structural" comparison
# (e.g., across clones)
prefix[i] = str(v)
try:
test.assertEqual(len(prefix_a), len(prefix_b))
for _a, _b in zip(prefix_a, prefix_b):
test.assertIs(_a.__class__, _b.__class__)
test.assertEqual(_a, _b)
except (PyomoException, AssertionError):
test.fail(f"Expressions not structurally equal:\n\t"
f"{tostr(prefix_a)}\n\t!=\n\t{tostr(prefix_b)}")
28 changes: 27 additions & 1 deletion pyomo/core/tests/unit/test_compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
InequalityExpression, EqualityExpression, RangedExpression
)
from pyomo.core.expr.compare import (
convert_expression_to_prefix_notation, compare_expressions
convert_expression_to_prefix_notation, compare_expressions,
assertExpressionsEqual, assertExpressionsStructurallyEqual,
)
from pyomo.common.getGSL import find_GSL

Expand Down Expand Up @@ -159,3 +160,28 @@ def test_ranged_expression(self):
m.x,
1]
self.assertEqual(pn, expected)

def test_assertExpressionsEqual(self):
m = pe.ConcreteModel()
m.x = pe.Var()
m.e1 = pe.Expression(expr=m.x**2 + m.x - 1)
m.e2 = pe.Expression(expr=m.x**2 + m.x - 1)
m.f = pe.Expression(expr=m.x**2 + 2*m.x - 1)
m.g = pe.Expression(expr=m.x**2 + m.x - 2)

assertExpressionsEqual(self, m.e1.expr, m.e2.expr)
assertExpressionsStructurallyEqual(self, m.e1.expr, m.e2.expr)
with self.assertRaisesRegex(
AssertionError, 'Expressions not equal:'):
assertExpressionsEqual(self, m.e1.expr, m.f.expr)
with self.assertRaisesRegex(
AssertionError, 'Expressions not structurally equal:'):
assertExpressionsStructurallyEqual(self, m.e1.expr, m.f.expr)

# Structurally equal will compare across clones, whereas strict
# equality will not
i = m.clone()
with self.assertRaisesRegex(
AssertionError, 'Expressions not equal:'):
assertExpressionsEqual(self, m.e1.expr, i.e1.expr)
assertExpressionsStructurallyEqual(self, m.e1.expr, i.e1.expr)
40 changes: 31 additions & 9 deletions pyomo/repn/plugins/cpxlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def _print_expr_canonical(self,
variable_symbol_dictionary,
is_objective,
column_order,
file_determinism,
force_objective_constant=False):

"""
Expand Down Expand Up @@ -211,16 +212,21 @@ def _print_expr_canonical(self,
# Order columns by dictionary names
#
names = [variable_symbol_dictionary[id(var)] for var in x.linear_vars]

for i, name in sorted(enumerate(names), key=lambda x: x[1]):
output.append(linear_coef_string_template % (x.linear_coefs[i], name))

term_iterator = zip(x.linear_coefs, names)
if file_determinism > 0:
term_iterator = sorted(term_iterator, key=lambda x: x[1])

for coef, name in term_iterator:
output.append(linear_coef_string_template % (coef, name))
else:
#
# Order columns by the value of column_order[]
#
for i, var in sorted(enumerate(x.linear_vars), key=lambda x: column_order[x[1]]):
name = variable_symbol_dictionary[id(var)]
output.append(linear_coef_string_template % (x.linear_coefs[i], name))

#
# Quadratic
#
Expand Down Expand Up @@ -250,7 +256,11 @@ def _print_expr_canonical(self,
quad.add(i)
names.append( (name1,name1) )
i += 1
for i, names_ in sorted(enumerate(names), key=lambda x: x[1]):

term_iterator = enumerate(names)
if file_determinism > 0:
term_iterator = sorted(term_iterator, key=lambda x: x[1])
for i, names_ in term_iterator:
#
# Times 2 because LP format requires /2 for all the quadratic
# terms /of the objective only/. Discovered the last bit thru
Expand Down Expand Up @@ -466,6 +476,7 @@ def print_expr_canonical(
variable_symbol_dictionary,
is_objective,
column_order,
file_determinism,
force_objective_constant=False):
try:
return self._print_expr_canonical(
Expand All @@ -475,6 +486,7 @@ def print_expr_canonical(
variable_symbol_dictionary=variable_symbol_dictionary,
is_objective=is_objective,
column_order=column_order,
file_determinism=file_determinism,
force_objective_constant=force_objective_constant)
except KeyError as e:
_id = e.args[0]
Expand Down Expand Up @@ -552,7 +564,10 @@ def print_expr_canonical(
if gen_obj_repn == False:
repn = block_repn[objective_data]
else:
repn = generate_standard_repn(objective_data.expr)
repn = generate_standard_repn(
objective_data.expr,
quadratic=supports_quadratic_objective,
)
if gen_obj_repn:
block_repn[objective_data] = repn

Expand Down Expand Up @@ -586,6 +601,7 @@ def print_expr_canonical(
variable_symbol_dictionary,
True,
column_order,
file_determinism,
force_objective_constant=force_objective_constant)

if numObj == 0:
Expand Down Expand Up @@ -637,7 +653,10 @@ def constraint_generator():
if constraint_data._linear_canonical_form:
repn = constraint_data.canonical_form()
else:
repn = generate_standard_repn(constraint_data.body)
repn = generate_standard_repn(
constraint_data.body,
quadratic=supports_quadratic_constraint,
)
if gen_con_repn:
block_repn[constraint_data] = repn

Expand Down Expand Up @@ -697,7 +716,8 @@ def yield_all_constraints():
object_symbol_dictionary,
variable_symbol_dictionary,
False,
column_order
column_order,
file_determinism
)
bound = constraint_data.lower
bound = _get_bound(bound) - offset
Expand All @@ -720,7 +740,8 @@ def yield_all_constraints():
object_symbol_dictionary,
variable_symbol_dictionary,
False,
column_order
column_order,
file_determinism
)
bound = constraint_data.lower
bound = _get_bound(bound) - offset
Expand All @@ -744,7 +765,8 @@ def yield_all_constraints():
object_symbol_dictionary,
variable_symbol_dictionary,
False,
column_order
column_order,
file_determinism
)
bound = constraint_data.upper
bound = _get_bound(bound) - offset
Expand Down
12 changes: 12 additions & 0 deletions pyomo/solvers/plugins/solvers/GAMS.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ def solve(self, *args, **kwds):

initial_time = time.time()

# Because GAMS changes the CWD when running the solver, we need
# to convert user-provided file names to absolute paths
# (relative to the current directory)
if logfile is not None:
logfile = os.path.abspath(logfile)

####################################################################
# Presolve
####################################################################
Expand Down Expand Up @@ -735,6 +741,12 @@ def solve(self, *args, **kwds):
# any unrecognized arguments
initial_time = time.time()

# Because GAMS changes the CWD when running the solver, we need
# to convert user-provided file names to absolute paths
# (relative to the current directory)
if logfile is not None:
logfile = os.path.abspath(logfile)

####################################################################
# Presolve
####################################################################
Expand Down
Loading

0 comments on commit 9f6c7be

Please sign in to comment.