Skip to content

Commit

Permalink
Merge branch 'master' into cython
Browse files Browse the repository at this point in the history
  • Loading branch information
jurgen-lentz committed Oct 11, 2024
2 parents e36d21e + 0efbdf7 commit c4ec7a0
Show file tree
Hide file tree
Showing 19 changed files with 237 additions and 39 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## 0.14.1 - 2024-05-##
- Add flat argument to AMPL.get_iis.

## 0.14.0 - 2024-05-21
- Allow assigning values to indexed sets from a dictionary with the lists of members
for every index.
- Add AMPL.get_iis() to return dictionaries with variables and constraints in IIS.
- Add AMPL.get_solution() to return a dictionary with the solution.

## 0.13.3 - 2024-02-20
- Fix issues with AMPL.solve(verbose=False) when the solver argument was not set.

## 0.13.2 - 2024-01-05
- OutputHandler: Flush standard output after every message.

Expand Down
2 changes: 1 addition & 1 deletion amplpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
except Exception:
pass

__version__ = "0.15.0"
__version__ = "0.15.0b0"


def _list_aliases():
Expand Down
90 changes: 90 additions & 0 deletions amplpy/ampl.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ from cpython.bool cimport PyBool_Check


from numbers import Real
from ast import literal_eval

include "util.pxi" # must be first
include "constraint.pxi"
Expand All @@ -26,6 +27,22 @@ include "set.pxi"
include "variable.pxi"


def nested_dict_of_suffixes(lst):
nested = {}
for name, value in lst:
if "[" not in name:
nested[name] = value
else:
p = name.find("[")
v, index = name[:p], literal_eval(f"({name[p+1:-1]},)")
if v not in nested:
nested[v] = {}
if len(index) == 1:
index = index[0]
nested[v][index] = value
return nested


AMPL_NOT_FOUND_MESSAGE = """
Please make sure that the AMPL directory is in the system search path, or
add it before instantiating the AMPL object with:
Expand Down Expand Up @@ -916,6 +933,79 @@ cdef class AMPL:
"""
return self.get_value("solve_result_num")

def get_iis(self, flat=True):
"""
Get IIS attributes for all variables and constraints.
Args:
flat: Return flat dictionaries if set to True, or nested dictionaries otherwise.
Returns:
Tuple with a dictionary for variables in the IIS and another for the constraints.
Usage example:
.. code-block:: python
from amplpy import AMPL
ampl = AMPL()
ampl.eval(
r\"\"\"
var x >= 0;
var y >= 0;
maximize obj: x+y;
s.t. s: x+y <= -5;
\"\"\"
)
ampl.option["presolve"] = 0 # disable AMPL presolve
ampl.solve(solver="gurobi", gurobi_options="outlev=1 iis=1")
if ampl.solve_result == "infeasible":
var_iis, con_iis = ampl.get_iis()
print(var_iis, con_iis)
"""
iis_var = self.get_data(
"{i in 1.._nvars: _var[i].iis != 'non'} (_varname[i], _var[i].iis)"
).to_list(skip_index=True)
iis_con = self.get_data(
"{i in 1.._ncons: _con[i].iis != 'non'} (_conname[i], _con[i].iis)"
).to_list(skip_index=True)
if flat is False:
iis_var = nested_dict_of_suffixes(iis_var)
iis_con = nested_dict_of_suffixes(iis_con)
else:
iis_var = dict(iis_var)
iis_con = dict(iis_con)
return iis_var, iis_con

def get_solution(self, flat=True, zeros=False):
"""
Get solution values for all variables.
Args:
flat: Return a flat dictionary if set to True, or a nested dictionary otherwise.
zeros: Include zeros in the solution if set to True.
Returns:
Returns a dictionary with the solution.
Usage example:
.. code-block:: python
ampl.solve(solver="gurobi", gurobi_options="outlev=0")
if ampl.solve_result == "solved":
print(ampl.get_solution())
"""
if zeros:
stmt = "{i in 1.._nvars} (_varname[i], _var[i].val)"
else:
stmt = "{i in 1.._nvars: _var[i].val != 0} (_varname[i], _var[i].val)"
lst_solution = self.get_data(stmt).to_list(skip_index=True)
if flat:
return dict(lst_solution)
return nested_dict_of_suffixes(lst_solution)

def _start_recording(self, filename):
"""
Start recording the session to a file for debug purposes.
Expand Down
6 changes: 5 additions & 1 deletion amplpy/errorhandler.pxi
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
try:
from .tools import _SUPPORT_MESSAGE
except Exception:
_SUPPORT_MESSAGE = ""

def display_error_message(exception, error=True):
msg = "\t" + str(exception).replace("\n", "\n\t")
if error:
print(f"Error:\n{msg}")
print(f"Error:\n{msg}{_SUPPORT_MESSAGE}")
else:
print(f"Warning:\n{msg}")

Expand Down
6 changes: 0 additions & 6 deletions amplpy/exceptions.pxi
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
# -*- coding: utf-8 -*-

try:
from .tools import _SUPPORT_MESSAGE
except Exception:
_SUPPORT_MESSAGE = ""


class AMPLException(Exception):
"""
Expand Down Expand Up @@ -45,7 +40,6 @@ class AMPLException(Exception):

def __str__(self):
return "file: " + self.source_name + self.message + _SUPPORT_MESSAGE #discuss with Filipe
#return self.what.lstrip("file: -").strip("\n") + _SUPPORT_MESSAGE

# Aliases
getLineNumber = get_line_number
Expand Down
11 changes: 8 additions & 3 deletions amplpy/set.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,16 @@ cdef class Set(Entity):
.. code-block:: python
A, AA = ampl.getSet('A'), ampl.getSet('AA')
AA.setValues(A.getValues()) # AA has now the members {1, 2}
A, AA = ampl.get_set('A'), ampl.get_set('AA')
AA.set_values(A.get_values()) # AA has now the members {1, 2}
"""
cdef DataFrame df = None
if isinstance(values, DataFrame):
if not self.is_scalar():
if not isinstance(values, dict):
raise TypeError("Excepted dictionary of set members for each index.")
for index, members in values.items():
self[index].set_values(members)
elif isinstance(values, DataFrame):
df = values
campl.AMPL_SetInstanceSetValuesDataframe(self._c_ampl, self._name.encode('utf-8'), self._index, df.get_ptr())
elif isinstance(values, Iterable):
Expand Down
56 changes: 54 additions & 2 deletions amplpy/tests/test_ampl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os

import amplpy
from amplpy import modules
from . import TestBase


Expand Down Expand Up @@ -384,8 +385,8 @@ def test_write(self):
ampl.write("bmod", "rc")

def test_solve_arguments(self):
if shutil.which("highs") is None:
self.skipTest("highs not available")
if "highs" not in modules.installed():
self.skipTest("highs is not available")
ampl = self.ampl
ampl.eval(
"""
Expand Down Expand Up @@ -418,6 +419,57 @@ def test_issue56(self):
ampl.get_output("for{i in 0..2} { display i;}"),
)

def test_get_iis(self):
if "gurobi" not in modules.installed():
self.skipTest("gurobi is not available")
ampl = self.ampl
ampl.eval(
r"""
var x >= 0;
var y{1..2} >= 0;
maximize obj: x+y[1]+y[2];
s.t. s: x+y[1] <= -5;
"""
)
ampl.option["presolve"] = 0
ampl.solve(solver="gurobi", gurobi_options="outlev=1 iis=1")
self.assertEqual(ampl.solve_result, "infeasible")
var_iis, con_iis = ampl.get_iis()
self.assertEqual(var_iis, {"x": "low", "y[1]": "low"})
self.assertEqual(con_iis, {"s": "mem"})
var_iis, con_iis = ampl.get_iis(flat=False)
self.assertEqual(var_iis, {"x": "low", "y": {1: "low"}})
self.assertEqual(con_iis, {"s": "mem"})

def test_get_solution(self):
if "highs" not in modules.installed():
self.skipTest("highs is not available")
ampl = self.ampl
ampl.eval(
r"""
set I := {1, 2, 'a', 'b'};
var x >= 0;
var y{I} >= 0 <= 5;
maximize obj: sum{i in I} y[i]-x;
"""
)
ampl.option["presolve"] = 0
ampl.solve(solver="highs", highs_options="outlev=1")
self.assertEqual(ampl.solve_result, "solved")
self.assertEqual(
ampl.get_solution(flat=False, zeros=True),
{"x": 0, "y": {1: 5, 2: 5, "a": 5, "b": 5}},
)
self.assertEqual(
ampl.get_solution(flat=False),
{"y": {1: 5, 2: 5, "a": 5, "b": 5}},
)
self.assertEqual(
ampl.get_solution(flat=True),
{"y[1]": 5, "y[2]": 5, "y['a']": 5, "y['b']": 5},
)


if __name__ == "__main__":
unittest.main()
18 changes: 18 additions & 0 deletions amplpy/tests/test_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,24 @@ def test_set_iterable(self):
with self.assertRaises(ValueError):
ampl.get_set("B").set_values([1, 2])

def test_indexed_set(self):
ampl = self.ampl
ampl.eval(
r"""
set I;
set J{I};
"""
)
self.assertEqual(ampl.set["I"].is_scalar(), True)
self.assertEqual(ampl.set["J"].is_scalar(), False)
ampl.set["I"] = range(10)
ampl.set["J"] = {i: range(i) for i in range(10)}
self.assertEqual(ampl.set["I"].size(), 10)
self.assertEqual(list(ampl.set["I"].members()), list(range(10)))
for i in range(10):
self.assertEqual(ampl.set["J"][i].size(), i)
self.assertEqual(list(ampl.set["J"][i].members()), list(range(i)))

def test_precision(self):
pi = 3.1415926535897932384626433832795028841971
ampl = self.ampl
Expand Down
3 changes: 3 additions & 0 deletions amplpy/vendor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## 0.7.5-2024-02-27
- Fix bug when the environment variable PATH is not set.

## 0.7.4-2024-01-05
- Update support message.

Expand Down
2 changes: 1 addition & 1 deletion amplpy/vendor/ampltools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
__version__ = "0.7.4"
__version__ = "0.7.5"
from .notebooks import (
ampl_notebook,
)
Expand Down
2 changes: 1 addition & 1 deletion amplpy/vendor/ampltools/modules/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
__version__ = "0.7.4"
__version__ = "0.7.5"

from .amplpypi import (
path,
Expand Down
6 changes: 3 additions & 3 deletions amplpy/vendor/ampltools/modules/amplpypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ def generate_requirements(modules=[]):


def _find_ampl_lic():
for path in os.environ["PATH"].split(os.pathsep):
for path in os.environ.get("PATH", "").split(os.pathsep):
ampl_lic = os.path.abspath(os.path.join(path, "ampl.lic"))
if os.path.isfile(ampl_lic):
return ampl_lic
Expand Down Expand Up @@ -335,7 +335,7 @@ def load_modules(modules=[], head=True, verbose=False):
"""
path_modules = []
path_others = []
for path in os.environ["PATH"].split(os.pathsep):
for path in os.environ.get("PATH", "").split(os.pathsep):
if path.endswith("bin") and "ampl_module_" in path:
path_modules.append(path)
else:
Expand Down Expand Up @@ -376,7 +376,7 @@ def unload_modules(modules=[]):
os.environ["PATH"] = os.pathsep.join(
[
path
for path in os.environ["PATH"].split(os.pathsep)
for path in os.environ.get("PATH", "").split(os.pathsep)
if not path.endswith(to_remove)
]
)
Expand Down
2 changes: 1 addition & 1 deletion amplpy/vendor/ampltools/tests/TestBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def setUp(self):
modules.uninstall()

def get_env_path(self):
return os.environ["PATH"]
return os.environ.get("PATH", "")

def get_env_path_list(self):
return self.get_env_path().split(os.pathsep)
Expand Down
4 changes: 2 additions & 2 deletions amplpy/vendor/ampltools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ def _is_valid_uuid(uuid):

def add_to_path(path, head=True):
if head:
os.environ["PATH"] = path + os.pathsep + os.environ["PATH"]
os.environ["PATH"] = path + os.pathsep + os.environ.get("PATH", "")
else:
os.environ["PATH"] = os.environ["PATH"] + os.pathsep + path
os.environ["PATH"] = os.environ.get("PATH", "") + os.pathsep + path


def register_magics(store_name="_ampl_cells", ampl_object=None, globals_=None):
Expand Down
2 changes: 1 addition & 1 deletion amplpy/vendor/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def ls_dir(base_dir):

setup(
name="ampltools",
version="0.7.4",
version="0.7.5",
description="AMPL Python Tools",
long_description=__doc__,
long_description_content_type="text/markdown",
Expand Down
Loading

0 comments on commit c4ec7a0

Please sign in to comment.