Skip to content

Commit dcf21b6

Browse files
Merge pull request #13 from samarthmistry/fix/misc
Add attributes and methods for VarDict1D and VarDictND; miscellaneous updates
2 parents 8503f74 + 6591f26 commit dcf21b6

17 files changed

Lines changed: 553 additions & 78 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Features
1616
* **Specialized data structures**: For defining index-sets, parameters, and decision variables — enabling concise and high-performance algebraic modeling.
1717
* **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.
1818
* **Type-complete interface**: Enables static type checking and intelligent auto-completion suggestions with modern IDEs — reducing type errors and improving development speed.
19-
* **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).
19+
* **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).
2020

2121
Links
2222
-----

docs/source/api_reference/parameters.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ Mapping operations
9292
ParamDictND.popitem
9393
ParamDictND.setdefault
9494

95+
Efficient subset selection
96+
--------------------------
97+
.. autosummary::
98+
99+
VarDictND.subset_keys
100+
VarDictND.subset_values
101+
95102
Views
96103
-----
97104
- ``ParamDictND.items()``

docs/source/api_reference/variables.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Attributes
3131
----------
3232
.. autosummary::
3333

34+
VarDict1D.model
35+
VarDict1D.vartype
3436
VarDict1D.key_name
3537
VarDict1D.value_name
3638

@@ -69,6 +71,8 @@ Attributes
6971
----------
7072
.. autosummary::
7173

74+
VarDictND.model
75+
VarDictND.vartype
7276
VarDictND.key_names
7377
VarDictND.value_name
7478

@@ -85,6 +89,13 @@ Mapping operations
8589
VarDictND.get
8690
VarDictND.lookup
8791

92+
Efficient subset selection
93+
--------------------------
94+
.. autosummary::
95+
96+
VarDictND.subset_keys
97+
VarDictND.subset_values
98+
8899
Views
89100
-----
90101
- ``VarDictND.items()``

pyproject.toml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,11 @@ exclude_also = [
183183
legacy_tox_ini = """
184184
[tox]
185185
min_version = 4.0
186-
env_list = py{310,311,312,313}, mypy, cpx{2010,2210,2211}, pd{15,20,X}
186+
env_list = py{310,311,312,313}, mypy, cpx{2010,2210,2211,2212}, pd{15,20,X}
187187
188188
[gh]
189189
python =
190-
3.10 = py310, mypy, cpx2010, cpx2210, cpx2211, pd15, pd20, pdX
190+
3.10 = py310, mypy, cpx{2010,2210,2211,2212}, pd{15,20,X}
191191
3.11 = py311
192192
3.12 = py312
193193
3.13 = py313
@@ -212,8 +212,8 @@ legacy_tox_ini = """
212212
mypy src
213213
mypy tests/typing_tests
214214
215-
[testenv:py{312,313}]
216-
# Since CPLEX runtime is not supported beyond python 3.11, we skip all runtime-based tests
215+
[testenv:py313]
216+
# Since CPLEX runtime is not supported beyond python 3.12, we skip all runtime-based tests
217217
description = Run doctests, unit tests ex. model functions, and typing tests
218218
extras = tests
219219
commands =
@@ -249,6 +249,15 @@ legacy_tox_ini = """
249249
pytest --basetemp="{env_tmp_dir}" --no-cov \
250250
tests/unit_tests/model_funcs/
251251
252+
[testenv:cpx2212]
253+
description = Run unit tests for model functions against CPLEX 22.1.2
254+
basepython = 3.10
255+
deps = cplex==22.1.2.0
256+
extras = tests
257+
commands =
258+
pytest --basetemp="{env_tmp_dir}" --no-cov \
259+
tests/unit_tests/model_funcs/
260+
252261
[testenv:pd15]
253262
description = Run unit tests and doctests for pandas accessors against pandas 1.5
254263
basepython = 3.10

src/docplex_extensions/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
modeling with DOcplex — IBM® Decision Optimization CPLEX® Modeling for Python.
1010
"""
1111

12-
__version__ = '1.2.0'
12+
__version__ = '1.3.0'
1313

1414
# Check required dependencies
1515
from importlib.util import find_spec as _find_spec

src/docplex_extensions/_dict_mixins.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def _get_repr_header(self) -> str:
172172
else:
173173
return f'{self.__class__.__name__}:'
174174

175-
def _subset_keys(self, *pattern: Any) -> list[ElemNDT]:
175+
def subset_keys(self, *pattern: Any) -> list[ElemNDT]:
176176
"""Get a subset of the N-dim tuple keys of the Dict with a wildcard pattern.
177177
178178
Parameters
@@ -202,7 +202,7 @@ def _subset_keys(self, *pattern: Any) -> list[ElemNDT]:
202202
except Exception as exc:
203203
self._reraise_exc_from_indexset(exc)
204204

205-
def _get_matching_values(self, *pattern: Any) -> tuple[ValT, ...]:
205+
def subset_values(self, *pattern: Any) -> list[ValT]:
206206
"""Get Dict values for all keys that match the wildcard pattern.
207207
208208
Parameters
@@ -214,7 +214,7 @@ def _get_matching_values(self, *pattern: Any) -> tuple[ValT, ...]:
214214
215215
Returns
216216
-------
217-
tuple
217+
list
218218
219219
Raises
220220
------
@@ -227,13 +227,13 @@ def _get_matching_values(self, *pattern: Any) -> tuple[ValT, ...]:
227227
ValueError
228228
If the pattern has no wildcard or all wildcards.
229229
"""
230-
keys = self._subset_keys(*pattern)
230+
keys = self.subset_keys(*pattern)
231231
match len(keys):
232232
case 0:
233-
res: tuple[ValT, ...] = tuple()
233+
res: list[ValT] = []
234234
case 1:
235-
res = (self[keys[0]],)
235+
res = [self[keys[0]]]
236236
case _:
237-
res = itemgetter(*keys)(self)
237+
res = list(itemgetter(*keys)(self))
238238

239239
return res

src/docplex_extensions/_model_funcs.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,18 @@ def solve(model: Model, **kwargs: Any) -> SolveSolution | None:
149149
if to_reopen:
150150
stream = open(stream._target.name, 'a')
151151

152-
# Write solution quality statistics
153-
log_footer = '\n' + ' Solution quality statistics '.center(div_len, '-') + '\n\n'
154-
log_footer += str(cplex.solution.get_quality_metrics())
155-
log_footer += '\n' + '-' * div_len + '\n'
156-
stream.write(log_footer)
152+
# Write solution quality statistics if CPLEX finds a feasible solution
153+
if solution is None:
154+
stream.write('\n' + '-' * div_len + '\n')
155+
else:
156+
log_footer = '\n' + ' Solution quality statistics '.center(div_len, '-') + '\n\n'
157+
log_footer += str(cplex.solution.get_quality_metrics())
158+
log_footer += '\n' + '-' * div_len + '\n'
159+
stream.write(log_footer)
160+
stream.flush()
157161

158162
# When logging to stream objects, close them at the end
159163
if to_reopen:
160-
stream.flush()
161164
stream.close()
162165

163166
return solution

src/docplex_extensions/_param_dicts.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -635,10 +635,10 @@ def _calc_stat(self, *pattern: Any, stat_func: str) -> int | float:
635635
"""
636636
self._check_for_calc_stat(stat_func)
637637
if stat_func == 'sum' and pattern:
638-
res: int | float = sum(self._get_matching_values(*pattern))
638+
res: int | float = sum(self.subset_values(*pattern))
639639
elif stat_func != 'sum' and pattern:
640640
try:
641-
res = getattr(statistics, stat_func)(self._get_matching_values(*pattern))
641+
res = getattr(statistics, stat_func)(self.subset_values(*pattern))
642642
except statistics.StatisticsError:
643643
res = 0
644644
else:

src/docplex_extensions/_var_dicts.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from docplex.mp.dvar import Var
1313
from docplex.mp.linear import LinearExpr, ZeroExpr
1414
from docplex.mp.model import Model
15+
from docplex.mp.vartype import VarType
1516

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

3942
# Private attributes
4043
# ------------------
4144
# _indexset : IndexSetBase
4245
# Index-set of keys.
43-
# _model : docplex.mp.model.Model
44-
# DOcplex model.
4546

46-
__slots__ = ('_indexset', '_model')
47+
__slots__ = ('_indexset', '_model', '_vartype')
4748

4849
def __init__(
49-
self, docpx_var_dict: dict[ElemT, VarT], /, *, indexset: IndexSetBase[ElemT], model: Model
50+
self,
51+
docpx_var_dict: dict[ElemT, VarT],
52+
/,
53+
*,
54+
indexset: IndexSetBase[ElemT],
55+
model: Model,
56+
vartype: VarType,
5057
) -> None:
5158
self._validate_docpx_var_dict(docpx_var_dict)
5259

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

6269
self._model = model
63-
"""DOcplex model."""
70+
self._vartype = vartype
6471

6572
super().__init__(docpx_var_dict)
6673

74+
@property
75+
def model(self) -> Model:
76+
"""DOcplex model associated with the variables.
77+
78+
Returns
79+
-------
80+
docplex.mp.model.Model
81+
"""
82+
return self._model
83+
84+
@property
85+
def vartype(self) -> VarType:
86+
"""DOcplex VarType corresponding to the variables.
87+
88+
Returns
89+
-------
90+
DOcplex variable type (subclass of docplex.mp.vartype.VarType)
91+
"""
92+
return self._vartype
93+
6794
@staticmethod
6895
def _validate_docpx_var_dict(docpx_var_dict: dict[ElemT, VarT]) -> None:
6996
"""Validate that the input is a populated dict of DOcplex variables.
@@ -147,8 +174,10 @@ class VarDict1D(VarDictBase[Elem1DT, VarT], Dict1DMixin[Elem1DT, VarT]):
147174
Dictionary of variable objects from docplex.
148175
indexset : IndexSet1D
149176
Keys of dictionary encapsulated in IndexSet1D.
150-
model : docplex.mp.Model
177+
model : docplex.mp.model.Model
151178
DOcplex model associated with the variable objects.
179+
vartype : docplex.mp.vartype.VarType
180+
DOcplex VarType corresponding to the variable objects.
152181
value_name : str, optional
153182
Name to refer to variables - not used internally, and solely for user reference.
154183
"""
@@ -157,8 +186,6 @@ class VarDict1D(VarDictBase[Elem1DT, VarT], Dict1DMixin[Elem1DT, VarT]):
157186
# ------------------
158187
# _indexset : IndexSet1D
159188
# Index-set of keys.
160-
# _model : docplex.mp.model.Model
161-
# DOcplex model.
162189

163190
__slots__ = ('_key_name', '_value_name')
164191

@@ -169,12 +196,13 @@ def __init__(
169196
/,
170197
*,
171198
model: Model,
199+
vartype: VarType,
172200
value_name: str | None = None,
173201
) -> None:
174202
self.key_name = indexset.name
175203
self.value_name = value_name
176204

177-
super().__init__(docpx_var_dict, indexset=indexset, model=model)
205+
super().__init__(docpx_var_dict, indexset=indexset, model=model, vartype=vartype)
178206

179207
def __new__(
180208
cls,
@@ -183,6 +211,7 @@ def __new__(
183211
/,
184212
*,
185213
model: Model,
214+
vartype: VarType,
186215
value_name: str | None = None,
187216
) -> VarDict1D[Elem1DT, VarT]:
188217
raise TypeError(
@@ -198,11 +227,14 @@ def _create(
198227
/,
199228
*,
200229
model: Model,
230+
vartype: VarType,
201231
value_name: str | None = None,
202232
) -> VarDict1D[Elem1DT, VarT]:
203233
# Private method to construt VarDict1D
204234
instance = super().__new__(cls)
205-
cls.__init__(instance, docpx_var_dict, indexset, model=model, value_name=value_name)
235+
cls.__init__(
236+
instance, docpx_var_dict, indexset, model=model, vartype=vartype, value_name=value_name
237+
)
206238
return instance
207239

208240
def __repr__(self) -> str:
@@ -282,7 +314,7 @@ def sum(self) -> LinearExpr | ZeroExpr:
282314
>>> node_select.sum()
283315
docplex.mp.LinearExpr(node-select_A+node-select_B+node-select_C)
284316
"""
285-
return self._model.sum_vars_all_different(self.values())
317+
return self.model.sum_vars_all_different(self.values())
286318

287319

288320
class VarDictND(VarDictBase[ElemNDT, VarT], DictNDMixin[ElemNDT, VarT]):
@@ -294,8 +326,10 @@ class VarDictND(VarDictBase[ElemNDT, VarT], DictNDMixin[ElemNDT, VarT]):
294326
Dictionary of variable objects from docplex.
295327
indexset : IndexSetND
296328
Keys of dictionary encapsulated in IndexSetND.
297-
model : docplex.mp.Model
329+
model : docplex.mp.model.Model
298330
DOcplex model associated with the variable objects.
331+
vartype : docplex.mp.vartype.VarType
332+
DOcplex VarType corresponding to the variable objects.
299333
value_name : str, optional
300334
Name to refer to variables - not used internally, and solely for user reference.
301335
"""
@@ -304,8 +338,6 @@ class VarDictND(VarDictBase[ElemNDT, VarT], DictNDMixin[ElemNDT, VarT]):
304338
# ------------------
305339
# _indexset : IndexSetND
306340
# Index-set of keys.
307-
# _model : docplex.mp.model.Model
308-
# DOcplex model.
309341

310342
__slots__ = ('_key_names', '_value_name')
311343

@@ -316,12 +348,13 @@ def __init__(
316348
/,
317349
*,
318350
model: Model,
351+
vartype: VarType,
319352
value_name: str | None = None,
320353
) -> None:
321354
self.key_names = indexset.names
322355
self.value_name = value_name
323356

324-
super().__init__(docpx_var_dict, indexset=indexset, model=model)
357+
super().__init__(docpx_var_dict, indexset=indexset, model=model, vartype=vartype)
325358

326359
def __new__(
327360
cls,
@@ -330,6 +363,7 @@ def __new__(
330363
/,
331364
*,
332365
model: Model,
366+
vartype: VarType,
333367
value_name: str | None = None,
334368
) -> VarDictND[ElemNDT, VarT]:
335369
raise TypeError(
@@ -345,11 +379,14 @@ def _create(
345379
/,
346380
*,
347381
model: Model,
382+
vartype: VarType,
348383
value_name: str | None = None,
349384
) -> VarDictND[ElemNDT, VarT]:
350385
# Private method to construt VarDictND
351386
instance = super().__new__(cls)
352-
cls.__init__(instance, docpx_var_dict, indexset, model=model, value_name=value_name)
387+
cls.__init__(
388+
instance, docpx_var_dict, indexset, model=model, vartype=vartype, value_name=value_name
389+
)
353390
return instance
354391

355392
def __repr__(self) -> str:
@@ -460,5 +497,5 @@ def sum(self, *pattern: Any) -> LinearExpr | ZeroExpr:
460497
docplex.mp.ZeroExpr()
461498
"""
462499
if pattern:
463-
return self._model.sum_vars_all_different(self._get_matching_values(*pattern))
464-
return self._model.sum_vars_all_different(self.values())
500+
return self.model.sum_vars_all_different(self.subset_values(*pattern))
501+
return self.model.sum_vars_all_different(self.values())

0 commit comments

Comments
 (0)