Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Features
* **Specialized data structures**: For defining index-sets, parameters, and decision variables — enabling concise and high-performance algebraic modeling.
* **Easy access to additional CPLEX functionality**: Like [tuning tool](https://www.ibm.com/docs/en/icos/latest?topic=programmingconsiderations-tuning-tool), [runseeds](https://www.ibm.com/docs/en/icos/latest?topic=cplex-evaluating-variability), [displaying problem statistics](https://www.ibm.com/docs/en/icos/latest?topic=problem-displaying-statistics) and [displaying solution quality statistics](https://www.ibm.com/docs/en/icos/latest?topic=cplex-evaluating-solution-quality) — not directly available in DOcplex.
* **Type-complete interface**: Enables static type checking and intelligent auto-completion suggestions with modern IDEs — reducing type errors and improving development speed.
* **Robust codebase**: 100% coverage spanning 1700+ test cases and fully type-checked with mypy under [strict mode](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration).
* **Robust codebase**: 100% coverage spanning 1800+ test cases and fully type-checked with mypy under [strict mode](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration).

Links
-----
Expand Down
7 changes: 7 additions & 0 deletions docs/source/api_reference/parameters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ Mapping operations
ParamDictND.popitem
ParamDictND.setdefault

Efficient subset selection
--------------------------
.. autosummary::

VarDictND.subset_keys
VarDictND.subset_values

Views
-----
- ``ParamDictND.items()``
Expand Down
11 changes: 11 additions & 0 deletions docs/source/api_reference/variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Attributes
----------
.. autosummary::

VarDict1D.model
VarDict1D.vartype
VarDict1D.key_name
VarDict1D.value_name

Expand Down Expand Up @@ -69,6 +71,8 @@ Attributes
----------
.. autosummary::

VarDictND.model
VarDictND.vartype
VarDictND.key_names
VarDictND.value_name

Expand All @@ -85,6 +89,13 @@ Mapping operations
VarDictND.get
VarDictND.lookup

Efficient subset selection
--------------------------
.. autosummary::

VarDictND.subset_keys
VarDictND.subset_values

Views
-----
- ``VarDictND.items()``
Expand Down
17 changes: 13 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,11 @@ exclude_also = [
legacy_tox_ini = """
[tox]
min_version = 4.0
env_list = py{310,311,312,313}, mypy, cpx{2010,2210,2211}, pd{15,20,X}
env_list = py{310,311,312,313}, mypy, cpx{2010,2210,2211,2212}, pd{15,20,X}

[gh]
python =
3.10 = py310, mypy, cpx2010, cpx2210, cpx2211, pd15, pd20, pdX
3.10 = py310, mypy, cpx{2010,2210,2211,2212}, pd{15,20,X}
3.11 = py311
3.12 = py312
3.13 = py313
Expand All @@ -212,8 +212,8 @@ legacy_tox_ini = """
mypy src
mypy tests/typing_tests

[testenv:py{312,313}]
# Since CPLEX runtime is not supported beyond python 3.11, we skip all runtime-based tests
[testenv:py313]
# Since CPLEX runtime is not supported beyond python 3.12, we skip all runtime-based tests
description = Run doctests, unit tests ex. model functions, and typing tests
extras = tests
commands =
Expand Down Expand Up @@ -249,6 +249,15 @@ legacy_tox_ini = """
pytest --basetemp="{env_tmp_dir}" --no-cov \
tests/unit_tests/model_funcs/

[testenv:cpx2212]
description = Run unit tests for model functions against CPLEX 22.1.2
basepython = 3.10
deps = cplex==22.1.2.0
extras = tests
commands =
pytest --basetemp="{env_tmp_dir}" --no-cov \
tests/unit_tests/model_funcs/

[testenv:pd15]
description = Run unit tests and doctests for pandas accessors against pandas 1.5
basepython = 3.10
Expand Down
2 changes: 1 addition & 1 deletion src/docplex_extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
modeling with DOcplex — IBM® Decision Optimization CPLEX® Modeling for Python.
"""

__version__ = '1.2.0'
__version__ = '1.3.0'

# Check required dependencies
from importlib.util import find_spec as _find_spec
Expand Down
14 changes: 7 additions & 7 deletions src/docplex_extensions/_dict_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def _get_repr_header(self) -> str:
else:
return f'{self.__class__.__name__}:'

def _subset_keys(self, *pattern: Any) -> list[ElemNDT]:
def subset_keys(self, *pattern: Any) -> list[ElemNDT]:
"""Get a subset of the N-dim tuple keys of the Dict with a wildcard pattern.

Parameters
Expand Down Expand Up @@ -202,7 +202,7 @@ def _subset_keys(self, *pattern: Any) -> list[ElemNDT]:
except Exception as exc:
self._reraise_exc_from_indexset(exc)

def _get_matching_values(self, *pattern: Any) -> tuple[ValT, ...]:
def subset_values(self, *pattern: Any) -> list[ValT]:
"""Get Dict values for all keys that match the wildcard pattern.

Parameters
Expand All @@ -214,7 +214,7 @@ def _get_matching_values(self, *pattern: Any) -> tuple[ValT, ...]:

Returns
-------
tuple
list

Raises
------
Expand All @@ -227,13 +227,13 @@ def _get_matching_values(self, *pattern: Any) -> tuple[ValT, ...]:
ValueError
If the pattern has no wildcard or all wildcards.
"""
keys = self._subset_keys(*pattern)
keys = self.subset_keys(*pattern)
match len(keys):
case 0:
res: tuple[ValT, ...] = tuple()
res: list[ValT] = []
case 1:
res = (self[keys[0]],)
res = [self[keys[0]]]
case _:
res = itemgetter(*keys)(self)
res = list(itemgetter(*keys)(self))

return res
15 changes: 9 additions & 6 deletions src/docplex_extensions/_model_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,18 @@ def solve(model: Model, **kwargs: Any) -> SolveSolution | None:
if to_reopen:
stream = open(stream._target.name, 'a')

# Write solution quality statistics
log_footer = '\n' + ' Solution quality statistics '.center(div_len, '-') + '\n\n'
log_footer += str(cplex.solution.get_quality_metrics())
log_footer += '\n' + '-' * div_len + '\n'
stream.write(log_footer)
# Write solution quality statistics if CPLEX finds a feasible solution
if solution is None:
stream.write('\n' + '-' * div_len + '\n')
else:
log_footer = '\n' + ' Solution quality statistics '.center(div_len, '-') + '\n\n'
log_footer += str(cplex.solution.get_quality_metrics())
log_footer += '\n' + '-' * div_len + '\n'
stream.write(log_footer)
stream.flush()

# When logging to stream objects, close them at the end
if to_reopen:
stream.flush()
stream.close()

return solution
Expand Down
4 changes: 2 additions & 2 deletions src/docplex_extensions/_param_dicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,10 +635,10 @@ def _calc_stat(self, *pattern: Any, stat_func: str) -> int | float:
"""
self._check_for_calc_stat(stat_func)
if stat_func == 'sum' and pattern:
res: int | float = sum(self._get_matching_values(*pattern))
res: int | float = sum(self.subset_values(*pattern))
elif stat_func != 'sum' and pattern:
try:
res = getattr(statistics, stat_func)(self._get_matching_values(*pattern))
res = getattr(statistics, stat_func)(self.subset_values(*pattern))
except statistics.StatisticsError:
res = 0
else:
Expand Down
75 changes: 56 additions & 19 deletions src/docplex_extensions/_var_dicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from docplex.mp.dvar import Var
from docplex.mp.linear import LinearExpr, ZeroExpr
from docplex.mp.model import Model
from docplex.mp.vartype import VarType

from ._dict_mixins import DefaultT, Dict1DMixin, DictBaseMixin, DictNDMixin
from ._index_sets import Elem1DT, ElemNDT, ElemT, IndexSet1D, IndexSetBase, IndexSetND
Expand All @@ -32,21 +33,27 @@ class VarDictBase(dict[ElemT, VarT], DictBaseMixin[ElemT, VarT]):
Dictionary of variable objects from docplex.
indexset : IndexSetBase
Keys of dictionary encapsulated in an IndexSet.
model : docplex.mp.Model
model : docplex.mp.model.Model
DOcplex model associated with the variable objects.
vartype : docplex.mp.vartype.VarType
DOcplex VarType corresponding to the variable objects.
"""

# Private attributes
# ------------------
# _indexset : IndexSetBase
# Index-set of keys.
# _model : docplex.mp.model.Model
# DOcplex model.

__slots__ = ('_indexset', '_model')
__slots__ = ('_indexset', '_model', '_vartype')

def __init__(
self, docpx_var_dict: dict[ElemT, VarT], /, *, indexset: IndexSetBase[ElemT], model: Model
self,
docpx_var_dict: dict[ElemT, VarT],
/,
*,
indexset: IndexSetBase[ElemT],
model: Model,
vartype: VarType,
) -> None:
self._validate_docpx_var_dict(docpx_var_dict)

Expand All @@ -60,10 +67,30 @@ def __init__(
"""Index-set of keys."""

self._model = model
"""DOcplex model."""
self._vartype = vartype

super().__init__(docpx_var_dict)

@property
def model(self) -> Model:
"""DOcplex model associated with the variables.

Returns
-------
docplex.mp.model.Model
"""
return self._model

@property
def vartype(self) -> VarType:
"""DOcplex VarType corresponding to the variables.

Returns
-------
DOcplex variable type (subclass of docplex.mp.vartype.VarType)
"""
return self._vartype

@staticmethod
def _validate_docpx_var_dict(docpx_var_dict: dict[ElemT, VarT]) -> None:
"""Validate that the input is a populated dict of DOcplex variables.
Expand Down Expand Up @@ -147,8 +174,10 @@ class VarDict1D(VarDictBase[Elem1DT, VarT], Dict1DMixin[Elem1DT, VarT]):
Dictionary of variable objects from docplex.
indexset : IndexSet1D
Keys of dictionary encapsulated in IndexSet1D.
model : docplex.mp.Model
model : docplex.mp.model.Model
DOcplex model associated with the variable objects.
vartype : docplex.mp.vartype.VarType
DOcplex VarType corresponding to the variable objects.
value_name : str, optional
Name to refer to variables - not used internally, and solely for user reference.
"""
Expand All @@ -157,8 +186,6 @@ class VarDict1D(VarDictBase[Elem1DT, VarT], Dict1DMixin[Elem1DT, VarT]):
# ------------------
# _indexset : IndexSet1D
# Index-set of keys.
# _model : docplex.mp.model.Model
# DOcplex model.

__slots__ = ('_key_name', '_value_name')

Expand All @@ -169,12 +196,13 @@ def __init__(
/,
*,
model: Model,
vartype: VarType,
value_name: str | None = None,
) -> None:
self.key_name = indexset.name
self.value_name = value_name

super().__init__(docpx_var_dict, indexset=indexset, model=model)
super().__init__(docpx_var_dict, indexset=indexset, model=model, vartype=vartype)

def __new__(
cls,
Expand All @@ -183,6 +211,7 @@ def __new__(
/,
*,
model: Model,
vartype: VarType,
value_name: str | None = None,
) -> VarDict1D[Elem1DT, VarT]:
raise TypeError(
Expand All @@ -198,11 +227,14 @@ def _create(
/,
*,
model: Model,
vartype: VarType,
value_name: str | None = None,
) -> VarDict1D[Elem1DT, VarT]:
# Private method to construt VarDict1D
instance = super().__new__(cls)
cls.__init__(instance, docpx_var_dict, indexset, model=model, value_name=value_name)
cls.__init__(
instance, docpx_var_dict, indexset, model=model, vartype=vartype, value_name=value_name
)
return instance

def __repr__(self) -> str:
Expand Down Expand Up @@ -282,7 +314,7 @@ def sum(self) -> LinearExpr | ZeroExpr:
>>> node_select.sum()
docplex.mp.LinearExpr(node-select_A+node-select_B+node-select_C)
"""
return self._model.sum_vars_all_different(self.values())
return self.model.sum_vars_all_different(self.values())


class VarDictND(VarDictBase[ElemNDT, VarT], DictNDMixin[ElemNDT, VarT]):
Expand All @@ -294,8 +326,10 @@ class VarDictND(VarDictBase[ElemNDT, VarT], DictNDMixin[ElemNDT, VarT]):
Dictionary of variable objects from docplex.
indexset : IndexSetND
Keys of dictionary encapsulated in IndexSetND.
model : docplex.mp.Model
model : docplex.mp.model.Model
DOcplex model associated with the variable objects.
vartype : docplex.mp.vartype.VarType
DOcplex VarType corresponding to the variable objects.
value_name : str, optional
Name to refer to variables - not used internally, and solely for user reference.
"""
Expand All @@ -304,8 +338,6 @@ class VarDictND(VarDictBase[ElemNDT, VarT], DictNDMixin[ElemNDT, VarT]):
# ------------------
# _indexset : IndexSetND
# Index-set of keys.
# _model : docplex.mp.model.Model
# DOcplex model.

__slots__ = ('_key_names', '_value_name')

Expand All @@ -316,12 +348,13 @@ def __init__(
/,
*,
model: Model,
vartype: VarType,
value_name: str | None = None,
) -> None:
self.key_names = indexset.names
self.value_name = value_name

super().__init__(docpx_var_dict, indexset=indexset, model=model)
super().__init__(docpx_var_dict, indexset=indexset, model=model, vartype=vartype)

def __new__(
cls,
Expand All @@ -330,6 +363,7 @@ def __new__(
/,
*,
model: Model,
vartype: VarType,
value_name: str | None = None,
) -> VarDictND[ElemNDT, VarT]:
raise TypeError(
Expand All @@ -345,11 +379,14 @@ def _create(
/,
*,
model: Model,
vartype: VarType,
value_name: str | None = None,
) -> VarDictND[ElemNDT, VarT]:
# Private method to construt VarDictND
instance = super().__new__(cls)
cls.__init__(instance, docpx_var_dict, indexset, model=model, value_name=value_name)
cls.__init__(
instance, docpx_var_dict, indexset, model=model, vartype=vartype, value_name=value_name
)
return instance

def __repr__(self) -> str:
Expand Down Expand Up @@ -460,5 +497,5 @@ def sum(self, *pattern: Any) -> LinearExpr | ZeroExpr:
docplex.mp.ZeroExpr()
"""
if pattern:
return self._model.sum_vars_all_different(self._get_matching_values(*pattern))
return self._model.sum_vars_all_different(self.values())
return self.model.sum_vars_all_different(self.subset_values(*pattern))
return self.model.sum_vars_all_different(self.values())
Loading