From 95c0250d414a99116c64606ee15e364b8d3bd7a4 Mon Sep 17 00:00:00 2001 From: Jakub Krasuski Date: Wed, 7 Oct 2026 13:31:38 +0200 Subject: [PATCH 1/2] fix(repr): show the last terms and a single constant in truncated expressions The tail of a truncated expression sliced the variable labels along the first axis, which for a quadratic expression is the factor axis, so the terms after the ellipsis repeated the first variables next to the last coefficients. The constant was also printed before the ellipsis as well as at the end. --- doc/release_notes.rst | 1 + linopy/common.py | 5 +++-- test/test_repr.py | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 4c8bb82f1..81c8fecd8 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 ``...``. **Breaking Changes** diff --git a/linopy/common.py b/linopy/common.py index 16b1da0c2..1649c1546 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 f97457b97..e3f57038e 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]) From 80651202eb58613d3e0cebe6ba158de008309824 Mon Sep 17 00:00:00 2001 From: Jakub Krasuski Date: Wed, 7 Oct 2026 19:37:42 +0200 Subject: [PATCH 2/2] docs: add the PR number to the release note --- doc/release_notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 81c8fecd8..6f41d7018 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -105,7 +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 ``...``. +* 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**