Skip to content

Commit 43af227

Browse files
FBumannclaudeFabianHofmann
authored
refac: replace print calls with str-returning format_* methods (#621)
* refac: replace print calls with str-returning format_* methods Add format_labels (Constraints, Variables) and format_infeasibilities (Model) that return strings instead of printing to stdout. Deprecate the old print_labels and print_infeasibilities methods with warnings pointing callers to the new alternatives. Closes #476 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add release note for format_labels/format_infeasibilities Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: note global options default for display_max_terms Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: rename print_single_constraint to format_single_constraint Align internal helper naming with the format_* convention introduced for the public API methods. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refac: rename remaining print_* helpers to format_* Rename print_coord, print_single_variable, print_single_expression, and print_line to use the format_* naming convention consistently. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Fabian Hofmann <fab.hof@gmx.de>
1 parent 5d278fb commit 43af227

7 files changed

Lines changed: 124 additions & 69 deletions

File tree

doc/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Upcoming Version
2424
* Improve handling of CPLEX solver quality attributes to ensure metrics such are extracted correctly when available.
2525
* Fix Xpress IIS label mapping for masked constraints and add a regression test for matching infeasible coordinates.
2626
* Enable quadratic problems with SCIP on windows.
27+
* Add ``format_labels()`` on ``Constraints``/``Variables`` and ``format_infeasibilities()`` on ``Model`` that return strings instead of printing to stdout, allowing usage with logging, storage, or custom output handling. Deprecate ``print_labels()`` and ``print_infeasibilities()``.
2728
* Add ``fix()``, ``unfix()``, and ``fixed`` to ``Variable`` and ``Variables`` for fixing variables to values via equality constraints. Supports automatic rounding for integer/binary variables.
2829
* Add ``relax()``, ``unrelax()``, and ``relaxed`` to ``Variable`` and ``Variables`` for LP relaxation of integer/binary variables. Supports partial relaxation via filtered views (e.g. ``m.variables.integers.relax()``). Semi-continuous variables raise ``NotImplementedError``.
2930

linopy/common.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,7 @@ def get_label_position(
986986
raise ValueError("Array's with more than two dimensions is not supported")
987987

988988

989-
def print_coord(coord: dict[str, Any] | Iterable[Any]) -> str:
989+
def format_coord(coord: dict[str, Any] | Iterable[Any]) -> str:
990990
"""
991991
Format coordinates into a string representation.
992992
@@ -999,11 +999,11 @@ def print_coord(coord: dict[str, Any] | Iterable[Any]) -> str:
999999
with nested coordinates grouped in parentheses.
10001000
10011001
Examples:
1002-
>>> print_coord({"x": 1, "y": 2})
1002+
>>> format_coord({"x": 1, "y": 2})
10031003
'[1, 2]'
1004-
>>> print_coord([1, 2, 3])
1004+
>>> format_coord([1, 2, 3])
10051005
'[1, 2, 3]'
1006-
>>> print_coord([(1, 2), (3, 4)])
1006+
>>> format_coord([(1, 2), (3, 4)])
10071007
'[(1, 2), (3, 4)]'
10081008
"""
10091009
# Handle empty input
@@ -1024,7 +1024,7 @@ def print_coord(coord: dict[str, Any] | Iterable[Any]) -> str:
10241024
return f"[{', '.join(formatted)}]"
10251025

10261026

1027-
def print_single_variable(model: Any, label: int) -> str:
1027+
def format_single_variable(model: Any, label: int) -> str:
10281028
if label == -1:
10291029
return "None"
10301030

@@ -1043,10 +1043,10 @@ def print_single_variable(model: Any, label: int) -> str:
10431043
else:
10441044
bounds = f" ∈ [{lower:.4g}, {upper:.4g}]"
10451045

1046-
return f"{name}{print_coord(coord)}{bounds}"
1046+
return f"{name}{format_coord(coord)}{bounds}"
10471047

10481048

1049-
def print_single_expression(
1049+
def format_single_expression(
10501050
c: np.ndarray,
10511051
v: np.ndarray,
10521052
const: float,
@@ -1058,7 +1058,7 @@ def print_single_expression(
10581058
c, v = np.atleast_1d(c), np.atleast_1d(v)
10591059

10601060
# catch case that to many terms would be printed
1061-
def print_line(
1061+
def format_line(
10621062
expr: list[tuple[float, tuple[str, Any] | list[tuple[str, Any]]]], const: float
10631063
) -> str:
10641064
res = []
@@ -1072,11 +1072,11 @@ def print_line(
10721072
var_string = ""
10731073
for name, coords in var:
10741074
if name is not None:
1075-
coord_string = print_coord(coords)
1075+
coord_string = format_coord(coords)
10761076
var_string += f" {name}{coord_string}"
10771077
else:
10781078
name, coords = var
1079-
coord_string = print_coord(coords)
1079+
coord_string = format_coord(coords)
10801080
var_string = f" {name}{coord_string}"
10811081

10821082
res.append(f"{coeff_string}{var_string}")
@@ -1103,23 +1103,23 @@ def print_line(
11031103
truncate = max_terms // 2
11041104
positions = model.variables.get_label_position(v[..., :truncate])
11051105
expr = list(zip(c[:truncate], positions))
1106-
res = print_line(expr, const)
1106+
res = format_line(expr, const)
11071107
res += " ... "
11081108
expr = list(
11091109
zip(
11101110
c[-truncate:],
11111111
model.variables.get_label_position(v[-truncate:]),
11121112
)
11131113
)
1114-
residual = print_line(expr, const)
1114+
residual = format_line(expr, const)
11151115
if residual != " None":
11161116
res += residual
11171117
return res
11181118
expr = list(zip(c, model.variables.get_label_position(v)))
1119-
return print_line(expr, const)
1119+
return format_line(expr, const)
11201120

11211121

1122-
def print_single_constraint(model: Any, label: int) -> str:
1122+
def format_single_constraint(model: Any, label: int) -> str:
11231123
constraints = model.constraints
11241124
name, coord = constraints.get_label_position(label)
11251125

@@ -1128,10 +1128,10 @@ def print_single_constraint(model: Any, label: int) -> str:
11281128
sign = model.constraints[name].sign.sel(coord).item()
11291129
rhs = model.constraints[name].rhs.sel(coord).item()
11301130

1131-
expr = print_single_expression(coeffs, vars, 0, model)
1131+
expr = format_single_expression(coeffs, vars, 0, model)
11321132
sign = SIGNS_pretty[sign]
11331133

1134-
return f"{name}{print_coord(coord)}: {expr} {sign} {rhs:.12g}"
1134+
return f"{name}{format_coord(coord)}: {expr} {sign} {rhs:.12g}"
11351135

11361136

11371137
def has_optimized_model(func: Callable[..., Any]) -> Callable[..., Any]:

linopy/constraints.py

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Any,
1616
overload,
1717
)
18+
from warnings import warn
1819

1920
import numpy as np
2021
import pandas as pd
@@ -36,6 +37,9 @@
3637
check_has_nulls,
3738
check_has_nulls_polars,
3839
filter_nulls_polars,
40+
format_coord,
41+
format_single_constraint,
42+
format_single_expression,
3943
format_string_as_variable_name,
4044
generate_indices_for_printout,
4145
get_dims_with_index_levels,
@@ -44,9 +48,6 @@
4448
iterate_slices,
4549
maybe_group_terms_polars,
4650
maybe_replace_signs,
47-
print_coord,
48-
print_single_constraint,
49-
print_single_expression,
5051
replace_by_map,
5152
require_constant,
5253
save_join,
@@ -304,17 +305,17 @@ def __repr__(self) -> str:
304305
for i, ind in enumerate(indices)
305306
]
306307
if self.mask is None or self.mask.values[indices]:
307-
expr = print_single_expression(
308+
expr = format_single_expression(
308309
self.coeffs.values[indices],
309310
self.vars.values[indices],
310311
0,
311312
self.model,
312313
)
313314
sign = SIGNS_pretty[self.sign.values[indices]]
314315
rhs = self.rhs.values[indices]
315-
line = print_coord(coord) + f": {expr} {sign} {rhs}"
316+
line = format_coord(coord) + f": {expr} {sign} {rhs}"
316317
else:
317-
line = print_coord(coord) + ": None"
318+
line = format_coord(coord) + ": None"
318319
lines.append(line)
319320
lines = align_lines_by_delimiter(lines, list(SIGNS_pretty.values()))
320321

@@ -323,7 +324,7 @@ def __repr__(self) -> str:
323324
underscore = "-" * (len(shape_str) + len(mask_str) + len(header_string) + 4)
324325
lines.insert(0, f"{header_string} [{shape_str}]{mask_str}:\n{underscore}")
325326
elif size == 1:
326-
expr = print_single_expression(
327+
expr = format_single_expression(
327328
self.coeffs.values, self.vars.values, 0, self.model
328329
)
329330
lines.append(
@@ -1016,29 +1017,47 @@ def get_label_position(
10161017
self._label_position_index = LabelPositionIndex(self)
10171018
return get_label_position(self, values, self._label_position_index)
10181019

1019-
def print_labels(
1020+
def format_labels(
10201021
self, values: Sequence[int], display_max_terms: int | None = None
1021-
) -> None:
1022+
) -> str:
10221023
"""
1023-
Print a selection of labels of the constraints.
1024+
Get a string representation of a selection of constraint labels.
10241025
10251026
Parameters
10261027
----------
10271028
values : list, array-like
10281029
One dimensional array of constraint labels.
1030+
display_max_terms : int, optional
1031+
Maximum number of terms to display per constraint. If ``None``,
1032+
uses the global ``linopy.options.display_max_terms`` setting.
1033+
1034+
Returns
1035+
-------
1036+
str
1037+
String representation of the selected constraints.
10291038
"""
10301039
with options as opts:
10311040
if display_max_terms is not None:
10321041
opts.set_value(display_max_terms=display_max_terms)
1033-
res = [print_single_constraint(self.model, v) for v in values]
1042+
res = [format_single_constraint(self.model, v) for v in values]
10341043

1035-
output = "\n".join(res)
1036-
try:
1037-
print(output)
1038-
except UnicodeEncodeError:
1039-
# Replace Unicode math symbols with ASCII equivalents for Windows console
1040-
output = output.replace("≤", "<=").replace("≥", ">=").replace("≠", "!=")
1041-
print(output)
1044+
return "\n".join(res)
1045+
1046+
def print_labels(
1047+
self, values: Sequence[int], display_max_terms: int | None = None
1048+
) -> None:
1049+
"""
1050+
Print a selection of labels of the constraints.
1051+
1052+
.. deprecated::
1053+
Use :meth:`format_labels` instead.
1054+
"""
1055+
warn(
1056+
"`Constraints.print_labels` is deprecated. Use `Constraints.format_labels` instead.",
1057+
DeprecationWarning,
1058+
stacklevel=2,
1059+
)
1060+
print(self.format_labels(values, display_max_terms=display_max_terms))
10421061

10431062
def set_blocks(self, block_map: np.ndarray) -> None:
10441063
"""
@@ -1157,7 +1176,7 @@ def __repr__(self) -> str:
11571176
"""
11581177
Get the representation of the AnonymousScalarConstraint.
11591178
"""
1160-
expr_string = print_single_expression(
1179+
expr_string = format_single_expression(
11611180
np.array(self.lhs.coeffs), np.array(self.lhs.vars), 0, self.lhs.model
11621181
)
11631182
return f"AnonymousScalarConstraint: {expr_string} {self.sign} {self.rhs}"

linopy/expressions.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
check_has_nulls_polars,
5454
fill_missing_coords,
5555
filter_nulls_polars,
56+
format_coord,
57+
format_single_expression,
5658
forward_as_properties,
5759
generate_indices_for_printout,
5860
get_dims_with_index_levels,
@@ -62,8 +64,6 @@
6264
is_constant,
6365
iterate_slices,
6466
maybe_group_terms_polars,
65-
print_coord,
66-
print_single_expression,
6767
to_dataframe,
6868
to_polars,
6969
)
@@ -429,24 +429,24 @@ def __repr__(self) -> str:
429429
self.data.indexes[dims[i]][ind] for i, ind in enumerate(indices)
430430
]
431431
if self.mask is None or self.mask.values[indices]:
432-
expr = print_single_expression(
432+
expr = format_single_expression(
433433
self.coeffs.values[indices],
434434
self.vars.values[indices],
435435
self.const.values[indices],
436436
self.model,
437437
)
438438

439-
line = print_coord(coord) + f": {expr}"
439+
line = format_coord(coord) + f": {expr}"
440440
else:
441-
line = print_coord(coord) + ": None"
441+
line = format_coord(coord) + ": None"
442442
lines.append(line)
443443

444444
shape_str = ", ".join(f"{d}: {s}" for d, s in zip(dim_names, dim_sizes))
445445
mask_str = f" - {masked_entries} masked entries" if masked_entries else ""
446446
underscore = "-" * (len(shape_str) + len(mask_str) + len(header_string) + 4)
447447
lines.insert(0, f"{header_string} [{shape_str}]{mask_str}:\n{underscore}")
448448
elif size == 1:
449-
expr = print_single_expression(
449+
expr = format_single_expression(
450450
self.coeffs.values, self.vars.values, self.const.item(), self.model
451451
)
452452
lines.append(f"{header_string}\n{'-' * len(header_string)}\n{expr}")
@@ -2470,7 +2470,7 @@ def __init__(
24702470
self._model = model
24712471

24722472
def __repr__(self) -> str:
2473-
expr_string = print_single_expression(
2473+
expr_string = format_single_expression(
24742474
np.array(self.coeffs), np.array(self.vars), 0, self.model
24752475
)
24762476
return f"ScalarLinearExpression: {expr_string}"

linopy/io.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ def signed_number(expr: pl.Expr) -> tuple[pl.Expr, pl.Expr]:
9090
)
9191

9292

93-
def print_coord(coord: str) -> str:
94-
from linopy.common import print_coord
93+
def format_coord(coord: str) -> str:
94+
from linopy.common import format_coord
9595

96-
coord = print_coord(coord).translate(coord_sanitizer)
96+
coord = format_coord(coord).translate(coord_sanitizer)
9797
return coord
9898

9999

@@ -106,12 +106,12 @@ def get_printers_scalar(
106106
def print_variable(var: Any) -> str:
107107
name, coord = m.variables.get_label_position(var)
108108
name = clean_name(name)
109-
return f"{name}{print_coord(coord)}#{var}"
109+
return f"{name}{format_coord(coord)}#{var}"
110110

111111
def print_constraint(cons: Any) -> str:
112112
name, coord = m.constraints.get_label_position(cons)
113113
name = clean_name(name) # type: ignore
114-
return f"{name}{print_coord(coord)}#{cons}" # type: ignore
114+
return f"{name}{format_coord(coord)}#{cons}" # type: ignore
115115

116116
return print_variable, print_constraint
117117
else:
@@ -134,12 +134,12 @@ def get_printers(
134134
def print_variable(var: Any) -> str:
135135
name, coord = m.variables.get_label_position(var)
136136
name = clean_name(name)
137-
return f"{name}{print_coord(coord)}#{var}"
137+
return f"{name}{format_coord(coord)}#{var}"
138138

139139
def print_constraint(cons: Any) -> str:
140140
name, coord = m.constraints.get_label_position(cons)
141141
name = clean_name(name) # type: ignore
142-
return f"{name}{print_coord(coord)}#{cons}" # type: ignore
142+
return f"{name}{format_coord(coord)}#{cons}" # type: ignore
143143

144144
def print_variable_series(series: pl.Series) -> tuple[pl.Expr, pl.Series]:
145145
return pl.lit(" "), series.map_elements(

0 commit comments

Comments
 (0)