Skip to content

Commit 0b58294

Browse files
authored
Merge pull request Pyomo#3554 from jsiirola/amplfunc-duplicates
Avoid duplicate `AMPLFUNC` entries in `ipopt_v2`
2 parents c1625b9 + 8f9b5be commit 0b58294

File tree

4 files changed

+101
-38
lines changed

4 files changed

+101
-38
lines changed

Diff for: pyomo/contrib/solver/solvers/ipopt.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from pyomo.core.expr.numvalue import value
4747
from pyomo.core.base.suffix import Suffix
4848
from pyomo.common.collections import ComponentMap
49+
from pyomo.solvers.amplfunc_merge import amplfunc_merge
4950

5051
logger = logging.getLogger(__name__)
5152

@@ -357,9 +358,9 @@ def solve(self, model, **kwds):
357358
# Get a copy of the environment to pass to the subprocess
358359
env = os.environ.copy()
359360
if nl_info.external_function_libraries:
360-
if env.get('AMPLFUNC'):
361-
nl_info.external_function_libraries.append(env.get('AMPLFUNC'))
362-
env['AMPLFUNC'] = "\n".join(nl_info.external_function_libraries)
361+
env['AMPLFUNC'] = amplfunc_merge(
362+
env, *nl_info.external_function_libraries
363+
)
363364
# Write the opt_file, if there should be one; return a bool to say
364365
# whether or not we have one (so we can correctly build the command line)
365366
opt_file = self._write_options_file(

Diff for: pyomo/core/tests/unit/test_external.py

+32
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,38 @@ def test_solve_gsl_function_const_arg(self):
585585
res = opt.solve(model, tee=True)
586586
self.assertAlmostEqual(value(model.x), 0.1, 5)
587587

588+
@unittest.skipIf(
589+
not check_available_solvers('ipopt_v2'),
590+
"The 'ipopt_v2' solver is not available",
591+
)
592+
def test_solve_gsl_function(self):
593+
DLL = find_GSL()
594+
if not DLL:
595+
self.skipTest("Could not find the amplgsl.dll library")
596+
model = ConcreteModel()
597+
model.z_func = ExternalFunction(library=DLL, function="gsl_sf_gamma")
598+
model.x = Var(initialize=3, bounds=(1e-5, None))
599+
model.o = Objective(expr=model.z_func(model.x))
600+
opt = SolverFactory('ipopt_v2')
601+
res = opt.solve(model, tee=True)
602+
self.assertAlmostEqual(value(model.o), 0.885603194411, 7)
603+
604+
@unittest.skipIf(
605+
not check_available_solvers('ipopt_v2'),
606+
"The 'ipopt_v2' solver is not available",
607+
)
608+
def test_solve_gsl_function_const_arg(self):
609+
DLL = find_GSL()
610+
if not DLL:
611+
self.skipTest("Could not find the amplgsl.dll library")
612+
model = ConcreteModel()
613+
model.z_func = ExternalFunction(library=DLL, function="gsl_sf_beta")
614+
model.x = Var(initialize=1, bounds=(0.1, None))
615+
model.o = Objective(expr=-model.z_func(1, model.x))
616+
opt = SolverFactory('ipopt_v2')
617+
res = opt.solve(model, tee=True)
618+
self.assertAlmostEqual(value(model.x), 0.1, 5)
619+
588620
@unittest.skipIf(
589621
not check_available_solvers('ipopt'), "The 'ipopt' solver is not available"
590622
)

Diff for: pyomo/solvers/amplfunc_merge.py

+50-20
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,54 @@
99
# This software is distributed under the 3-clause BSD License.
1010
# ___________________________________________________________________________
1111

12+
from pyomo.common.deprecation import relocated_module_attribute
1213

13-
def amplfunc_string_merge(amplfunc, pyomo_amplfunc):
14-
"""Merge two AMPLFUNC variable strings eliminating duplicate lines"""
15-
# Assume that the strings amplfunc and pyomo_amplfunc don't contain duplicates
16-
# Assume that the path separator is correct for the OS so we don't need to
17-
# worry about comparing Unix and Windows paths.
18-
amplfunc_lines = amplfunc.split("\n")
19-
existing = set(amplfunc_lines)
20-
for line in pyomo_amplfunc.split("\n"):
21-
# Skip lines we already have
22-
if line not in existing:
23-
amplfunc_lines.append(line)
24-
# Remove empty lines which could happen if one or both of the strings is
25-
# empty or there are two new lines in a row for whatever reason.
26-
amplfunc_lines = [s for s in amplfunc_lines if s != ""]
27-
return "\n".join(amplfunc_lines)
28-
29-
30-
def amplfunc_merge(env):
31-
"""Merge AMPLFUNC and PYOMO_AMPLFUNC in an environment var dict"""
32-
return amplfunc_string_merge(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", ""))
14+
relocated_module_attribute(
15+
'amplfunc_string_merge',
16+
'pyomo.solvers.amplfunc_merge.unique_paths',
17+
version='6.9.2.dev0',
18+
f_globals=globals(),
19+
)
20+
21+
22+
def unique_paths(*paths):
23+
"""Merge paths, eliminating duplicates.
24+
25+
Parameters
26+
----------
27+
*path : str
28+
29+
Each argument is a string containing one or more paths.
30+
Multiple libraries are separated by newlines.
31+
32+
Returns
33+
-------
34+
str : merged list of newline-separated paths.
35+
36+
"""
37+
funcs = {}
38+
for src in paths:
39+
for line in src.splitlines():
40+
if not line:
41+
continue
42+
funcs[line] = None
43+
return "\n".join(funcs)
44+
45+
46+
def amplfunc_merge(env, *funcs):
47+
"""Merge AMPLFUNC and PYOMO_AMPLFUNC environment variables with provided values
48+
49+
Paths are returned with entries from AMPLFUNC first, PYOMO_AMPLFUNC
50+
second, and the user-provided arguments last. Duplicates and empty
51+
paths are filtered out.
52+
53+
Parameters
54+
----------
55+
env : Dict[str, str]
56+
Environment dictionary mapping environment variable names to values.
57+
58+
*funcs : str
59+
Additional paths to combine.
60+
61+
"""
62+
return unique_paths(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", ""), *funcs)

Diff for: pyomo/solvers/tests/checks/test_amplfunc_merge.py

+15-15
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
# ___________________________________________________________________________
1111

1212
import pyomo.common.unittest as unittest
13-
from pyomo.solvers.amplfunc_merge import amplfunc_string_merge, amplfunc_merge
13+
from pyomo.solvers.amplfunc_merge import unique_paths, amplfunc_merge
1414

1515

1616
class TestAMPLFUNCStringMerge(unittest.TestCase):
1717
def test_merge_no_dup(self):
1818
s1 = "my/place/l1.so\nanother/place/l1.so"
1919
s2 = "my/place/l2.so"
20-
sm = amplfunc_string_merge(s1, s2)
20+
sm = unique_paths(s1, s2)
2121
sm_list = sm.split("\n")
2222
self.assertEqual(len(sm_list), 3)
2323
# The order of lines should be maintained with the second string
@@ -29,40 +29,40 @@ def test_merge_no_dup(self):
2929
def test_merge_empty1(self):
3030
s1 = ""
3131
s2 = "my/place/l2.so"
32-
sm = amplfunc_string_merge(s1, s2)
32+
sm = unique_paths(s1, s2)
3333
sm_list = sm.split("\n")
3434
self.assertEqual(len(sm_list), 1)
3535
self.assertEqual(sm_list[0], "my/place/l2.so")
3636

3737
def test_merge_empty2(self):
3838
s1 = "my/place/l2.so"
3939
s2 = ""
40-
sm = amplfunc_string_merge(s1, s2)
40+
sm = unique_paths(s1, s2)
4141
sm_list = sm.split("\n")
4242
self.assertEqual(len(sm_list), 1)
4343
self.assertEqual(sm_list[0], "my/place/l2.so")
4444

4545
def test_merge_empty_both(self):
4646
s1 = ""
4747
s2 = ""
48-
sm = amplfunc_string_merge(s1, s2)
48+
sm = unique_paths(s1, s2)
4949
sm_list = sm.split("\n")
5050
self.assertEqual(len(sm_list), 1)
5151
self.assertEqual(sm_list[0], "")
5252

5353
def test_merge_bad_type(self):
54-
self.assertRaises(AttributeError, amplfunc_string_merge, "", 3)
55-
self.assertRaises(AttributeError, amplfunc_string_merge, 3, "")
56-
self.assertRaises(AttributeError, amplfunc_string_merge, 3, 3)
57-
self.assertRaises(AttributeError, amplfunc_string_merge, None, "")
58-
self.assertRaises(AttributeError, amplfunc_string_merge, "", None)
59-
self.assertRaises(AttributeError, amplfunc_string_merge, 2.3, "")
60-
self.assertRaises(AttributeError, amplfunc_string_merge, "", 2.3)
54+
self.assertRaises(AttributeError, unique_paths, "", 3)
55+
self.assertRaises(AttributeError, unique_paths, 3, "")
56+
self.assertRaises(AttributeError, unique_paths, 3, 3)
57+
self.assertRaises(AttributeError, unique_paths, None, "")
58+
self.assertRaises(AttributeError, unique_paths, "", None)
59+
self.assertRaises(AttributeError, unique_paths, 2.3, "")
60+
self.assertRaises(AttributeError, unique_paths, "", 2.3)
6161

6262
def test_merge_duplicate1(self):
6363
s1 = "my/place/l1.so\nanother/place/l1.so"
6464
s2 = "my/place/l1.so\nanother/place/l1.so"
65-
sm = amplfunc_string_merge(s1, s2)
65+
sm = unique_paths(s1, s2)
6666
sm_list = sm.split("\n")
6767
self.assertEqual(len(sm_list), 2)
6868
# The order of lines should be maintained with the second string
@@ -73,7 +73,7 @@ def test_merge_duplicate1(self):
7373
def test_merge_duplicate2(self):
7474
s1 = "my/place/l1.so\nanother/place/l1.so"
7575
s2 = "my/place/l1.so"
76-
sm = amplfunc_string_merge(s1, s2)
76+
sm = unique_paths(s1, s2)
7777
sm_list = sm.split("\n")
7878
self.assertEqual(len(sm_list), 2)
7979
# The order of lines should be maintained with the second string
@@ -84,7 +84,7 @@ def test_merge_duplicate2(self):
8484
def test_merge_extra_linebreaks(self):
8585
s1 = "\nmy/place/l1.so\nanother/place/l1.so\n"
8686
s2 = "\nmy/place/l1.so\n\n"
87-
sm = amplfunc_string_merge(s1, s2)
87+
sm = unique_paths(s1, s2)
8888
sm_list = sm.split("\n")
8989
self.assertEqual(len(sm_list), 2)
9090
# The order of lines should be maintained with the second string

0 commit comments

Comments
 (0)