Skip to content
Open
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
1 change: 1 addition & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Upcoming Version
* Improve handling of CPLEX solver quality attributes to ensure metrics such are extracted correctly when available.
* Fix Xpress IIS label mapping for masked constraints and add a regression test for matching infeasible coordinates.
* Enable quadratic problems with SCIP on windows.
* 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()``.


Version 0.6.5
Expand Down
32 changes: 16 additions & 16 deletions linopy/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,7 @@ def get_label_position(
raise ValueError("Array's with more than two dimensions is not supported")


def print_coord(coord: dict[str, Any] | Iterable[Any]) -> str:
def format_coord(coord: dict[str, Any] | Iterable[Any]) -> str:
"""
Format coordinates into a string representation.

Expand All @@ -999,11 +999,11 @@ def print_coord(coord: dict[str, Any] | Iterable[Any]) -> str:
with nested coordinates grouped in parentheses.

Examples:
>>> print_coord({"x": 1, "y": 2})
>>> format_coord({"x": 1, "y": 2})
'[1, 2]'
>>> print_coord([1, 2, 3])
>>> format_coord([1, 2, 3])
'[1, 2, 3]'
>>> print_coord([(1, 2), (3, 4)])
>>> format_coord([(1, 2), (3, 4)])
'[(1, 2), (3, 4)]'
"""
# Handle empty input
Expand All @@ -1024,7 +1024,7 @@ def print_coord(coord: dict[str, Any] | Iterable[Any]) -> str:
return f"[{', '.join(formatted)}]"


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

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

return f"{name}{print_coord(coord)}{bounds}"
return f"{name}{format_coord(coord)}{bounds}"


def print_single_expression(
def format_single_expression(
c: np.ndarray,
v: np.ndarray,
const: float,
Expand All @@ -1058,7 +1058,7 @@ def print_single_expression(
c, v = np.atleast_1d(c), np.atleast_1d(v)

# catch case that to many terms would be printed
def print_line(
def format_line(
expr: list[tuple[float, tuple[str, Any] | list[tuple[str, Any]]]], const: float
) -> str:
res = []
Expand All @@ -1072,11 +1072,11 @@ def print_line(
var_string = ""
for name, coords in var:
if name is not None:
coord_string = print_coord(coords)
coord_string = format_coord(coords)
var_string += f" {name}{coord_string}"
else:
name, coords = var
coord_string = print_coord(coords)
coord_string = format_coord(coords)
var_string = f" {name}{coord_string}"

res.append(f"{coeff_string}{var_string}")
Expand All @@ -1103,23 +1103,23 @@ def print_line(
truncate = max_terms // 2
positions = model.variables.get_label_position(v[..., :truncate])
expr = list(zip(c[:truncate], positions))
res = print_line(expr, const)
res = format_line(expr, const)
res += " ... "
expr = list(
zip(
c[-truncate:],
model.variables.get_label_position(v[-truncate:]),
)
)
residual = print_line(expr, const)
residual = format_line(expr, const)
if residual != " None":
res += residual
return res
expr = list(zip(c, model.variables.get_label_position(v)))
return print_line(expr, const)
return format_line(expr, const)


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

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

expr = print_single_expression(coeffs, vars, 0, model)
expr = format_single_expression(coeffs, vars, 0, model)
sign = SIGNS_pretty[sign]

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


def has_optimized_model(func: Callable[..., Any]) -> Callable[..., Any]:
Expand Down
57 changes: 38 additions & 19 deletions linopy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Any,
overload,
)
from warnings import warn

import numpy as np
import pandas as pd
Expand All @@ -36,6 +37,9 @@
check_has_nulls,
check_has_nulls_polars,
filter_nulls_polars,
format_coord,
format_single_constraint,
format_single_expression,
format_string_as_variable_name,
generate_indices_for_printout,
get_dims_with_index_levels,
Expand All @@ -44,9 +48,6 @@
iterate_slices,
maybe_group_terms_polars,
maybe_replace_signs,
print_coord,
print_single_constraint,
print_single_expression,
replace_by_map,
require_constant,
save_join,
Expand Down Expand Up @@ -304,17 +305,17 @@ def __repr__(self) -> str:
for i, ind in enumerate(indices)
]
if self.mask is None or self.mask.values[indices]:
expr = print_single_expression(
expr = format_single_expression(
self.coeffs.values[indices],
self.vars.values[indices],
0,
self.model,
)
sign = SIGNS_pretty[self.sign.values[indices]]
rhs = self.rhs.values[indices]
line = print_coord(coord) + f": {expr} {sign} {rhs}"
line = format_coord(coord) + f": {expr} {sign} {rhs}"
else:
line = print_coord(coord) + ": None"
line = format_coord(coord) + ": None"
lines.append(line)
lines = align_lines_by_delimiter(lines, list(SIGNS_pretty.values()))

Expand All @@ -323,7 +324,7 @@ def __repr__(self) -> str:
underscore = "-" * (len(shape_str) + len(mask_str) + len(header_string) + 4)
lines.insert(0, f"{header_string} [{shape_str}]{mask_str}:\n{underscore}")
elif size == 1:
expr = print_single_expression(
expr = format_single_expression(
self.coeffs.values, self.vars.values, 0, self.model
)
lines.append(
Expand Down Expand Up @@ -1016,29 +1017,47 @@ def get_label_position(
self._label_position_index = LabelPositionIndex(self)
return get_label_position(self, values, self._label_position_index)

def print_labels(
def format_labels(
self, values: Sequence[int], display_max_terms: int | None = None
) -> None:
) -> str:
"""
Print a selection of labels of the constraints.
Get a string representation of a selection of constraint labels.

Parameters
----------
values : list, array-like
One dimensional array of constraint labels.
display_max_terms : int, optional
Maximum number of terms to display per constraint. If ``None``,
uses the global ``linopy.options.display_max_terms`` setting.

Returns
-------
str
String representation of the selected constraints.
"""
with options as opts:
if display_max_terms is not None:
opts.set_value(display_max_terms=display_max_terms)
res = [print_single_constraint(self.model, v) for v in values]
res = [format_single_constraint(self.model, v) for v in values]

output = "\n".join(res)
try:
print(output)
except UnicodeEncodeError:
# Replace Unicode math symbols with ASCII equivalents for Windows console
output = output.replace("≤", "<=").replace("≥", ">=").replace("≠", "!=")
print(output)
return "\n".join(res)

def print_labels(
self, values: Sequence[int], display_max_terms: int | None = None
) -> None:
"""
Print a selection of labels of the constraints.

.. deprecated::
Use :meth:`format_labels` instead.
"""
warn(
"`Constraints.print_labels` is deprecated. Use `Constraints.format_labels` instead.",
DeprecationWarning,
stacklevel=2,
)
print(self.format_labels(values, display_max_terms=display_max_terms))

def set_blocks(self, block_map: np.ndarray) -> None:
"""
Expand Down Expand Up @@ -1157,7 +1176,7 @@ def __repr__(self) -> str:
"""
Get the representation of the AnonymousScalarConstraint.
"""
expr_string = print_single_expression(
expr_string = format_single_expression(
np.array(self.lhs.coeffs), np.array(self.lhs.vars), 0, self.lhs.model
)
return f"AnonymousScalarConstraint: {expr_string} {self.sign} {self.rhs}"
Expand Down
14 changes: 7 additions & 7 deletions linopy/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
check_has_nulls_polars,
fill_missing_coords,
filter_nulls_polars,
format_coord,
format_single_expression,
forward_as_properties,
generate_indices_for_printout,
get_dims_with_index_levels,
Expand All @@ -62,8 +64,6 @@
is_constant,
iterate_slices,
maybe_group_terms_polars,
print_coord,
print_single_expression,
to_dataframe,
to_polars,
)
Expand Down Expand Up @@ -429,24 +429,24 @@ def __repr__(self) -> str:
self.data.indexes[dims[i]][ind] for i, ind in enumerate(indices)
]
if self.mask is None or self.mask.values[indices]:
expr = print_single_expression(
expr = format_single_expression(
self.coeffs.values[indices],
self.vars.values[indices],
self.const.values[indices],
self.model,
)

line = print_coord(coord) + f": {expr}"
line = format_coord(coord) + f": {expr}"
else:
line = print_coord(coord) + ": None"
line = format_coord(coord) + ": None"
lines.append(line)

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

def __repr__(self) -> str:
expr_string = print_single_expression(
expr_string = format_single_expression(
np.array(self.coeffs), np.array(self.vars), 0, self.model
)
return f"ScalarLinearExpression: {expr_string}"
Expand Down
14 changes: 7 additions & 7 deletions linopy/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ def signed_number(expr: pl.Expr) -> tuple[pl.Expr, pl.Expr]:
)


def print_coord(coord: str) -> str:
from linopy.common import print_coord
def format_coord(coord: str) -> str:
from linopy.common import format_coord

coord = print_coord(coord).translate(coord_sanitizer)
coord = format_coord(coord).translate(coord_sanitizer)
return coord


Expand All @@ -105,12 +105,12 @@ def get_printers_scalar(
def print_variable(var: Any) -> str:
name, coord = m.variables.get_label_position(var)
name = clean_name(name)
return f"{name}{print_coord(coord)}#{var}"
return f"{name}{format_coord(coord)}#{var}"

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

return print_variable, print_constraint
else:
Expand All @@ -133,12 +133,12 @@ def get_printers(
def print_variable(var: Any) -> str:
name, coord = m.variables.get_label_position(var)
name = clean_name(name)
return f"{name}{print_coord(coord)}#{var}"
return f"{name}{format_coord(coord)}#{var}"

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

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