diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 4c8bb82f..6f41d701 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -105,6 +105,7 @@ Upcoming Version * A frozen constraint caches the label-to-position mapping of its matrix columns by weak reference and only rebuilds it when the constraint or the set of variables changes. While a persistent snapshot holds the arrays, repeated matrix assembly on an unchanged model returns the same objects, so the snapshot diff can again skip the comparison of untouched frozen constraints by object identity; one-off exports such as ``to_file`` retain no extra memory. (`#933 `__) * ``Model.copy`` (and the ``copy.copy``/``copy.deepcopy`` protocols) keeps frozen constraints as ``CSRConstraint`` instead of rebuilding them as dense ``Constraint``; ``deep=True`` copies their arrays, and duals are copied only with ``include_solution=True``. A CSR-backed objective is now deep-copied with ``deep=True`` as well. The auxiliary coordinates of a CSR grid are stored read-only, so a copy can share them safely. (`#981 `__) * ``Solver.close()`` no longer leaves dangling native handles behind. The solver model is now dropped before the environment that owns it, instead of after. And the COPT and MindOpt file interfaces no longer hand back a model they already disposed: after a file-based COPT or MindOpt solve, ``model.solver_model`` is ``None`` rather than a handle into freed memory. (`#899 `__) +* The repr of an expression with more terms than ``linopy.options["display_max_terms"]`` now shows the last terms of a ``QuadraticExpression`` after the ``...`` instead of repeating the first ones, and prints the constant once at the end instead of also before the ``...``. (`#1023 `__) **Breaking Changes** diff --git a/linopy/common.py b/linopy/common.py index 16b1da0c..1649c154 100644 --- a/linopy/common.py +++ b/linopy/common.py @@ -1158,12 +1158,13 @@ def format_line( truncate = max_terms // 2 positions = model.variables.get_label_position(v[..., :truncate]) expr = list(zip(c[:truncate], positions)) - res = format_line(expr, const) + # the constant is printed once, after the last terms + res = format_line(expr, np.nan) res += " ... " expr = list( zip( c[-truncate:], - model.variables.get_label_position(v[-truncate:]), + model.variables.get_label_position(v[..., -truncate:]), ) ) residual = format_line(expr, const) diff --git a/test/test_repr.py b/test/test_repr.py index f97457b9..e3f57038 100644 --- a/test/test_repr.py +++ b/test/test_repr.py @@ -184,6 +184,21 @@ def test_print_options(obj: Variable | LinearExpression | Constraint) -> None: obj.print(display_max_rows=20) +def test_truncated_expression_repr_prints_constant_once() -> None: + expr = (np.arange(1, 11) * x).sum() + 5 + assert repr(expr).splitlines()[-1] == ( + "+1 x[0] + 2 x[1] + 3 x[2] ... +8 x[7] + 9 x[8] + 10 x[9] + 5" + ) + + +def test_truncated_quadratic_expression_repr_shows_last_terms() -> None: + expr = (np.arange(1, 11) * x * a).sum() + assert repr(expr).splitlines()[-1] == ( + "+1 x[0] a[0] + 2 x[1] a[1] + 3 x[2] a[2] ... " + "+8 x[7] a[7] + 9 x[8] a[8] + 10 x[9] a[9]" + ) + + def test_format_labels() -> None: assert m.variables.format_labels([1, 2, 3]) assert m.constraints.format_labels([1, 2, 3])