Skip to content

Add algebraic __str__ and detailed __repr__ to Python LP API classes#1400

Open
jackthepunished wants to merge 2 commits into
NVIDIA:mainfrom
jackthepunished:feature/str-repr-for-lp-api
Open

Add algebraic __str__ and detailed __repr__ to Python LP API classes#1400
jackthepunished wants to merge 2 commits into
NVIDIA:mainfrom
jackthepunished:feature/str-repr-for-lp-api

Conversation

@jackthepunished

@jackthepunished jackthepunished commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

The Python LP modeling classes (Variable, LinearExpression, QuadraticExpression, Constraint, Problem) currently fall back to <cuopt.linear_programming.problem.X object at 0x...> when printed, which makes model construction hard to verify in notebooks and REPLs. This adds str (algebraic form, e.g. 2.0 * x + 3.0 * y <= 10.0) and repr (detailed summary with bounds, type, and variable/constraint counts and solve status) to all five classes. Purely additive; covered by est_str_and_repr in est_python_API.py.

Adds __str__ and __repr__ to Variable, LinearExpression,

QuadraticExpression, Constraint, and Problem. Printing these objects now

shows their algebraic form (e.g. '2.0 * x + 3.0 * y <= 10.0') and the REPL

shows a detailed summary, improving debuggability in notebooks and

interactive sessions. The change is purely additive.

Signed-off-by: jackthepunished <kosapinarbahadir@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Jun 6, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@jackthepunished jackthepunished marked this pull request as ready for review June 6, 2026 00:36
@jackthepunished jackthepunished requested a review from a team as a code owner June 6, 2026 00:36
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6742bae3-ed1f-44e2-9335-e6e6dd97d2fb

📥 Commits

Reviewing files that changed from the base of the PR and between aca0220 and cd6c5ca.

📒 Files selected for processing (2)
  • python/cuopt/cuopt/linear_programming/problem.py
  • python/cuopt/cuopt/tests/linear_programming/test_python_API.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/cuopt/cuopt/tests/linear_programming/test_python_API.py
  • python/cuopt/cuopt/linear_programming/problem.py

📝 Walkthrough

Walkthrough

This PR adds human-readable __str__ and __repr__ methods for LP objects, shared expression-formatting helpers, constraint display handling, problem summaries, and tests covering exact formatting and truncation.

Changes

Human-readable display support for LP API objects

Layer / File(s) Summary
Internal formatting utilities
python/cuopt/cuopt/linear_programming/problem.py
Module-level helpers define sense and type labels, expression term limits, variable naming and type formatting, and shared expression string building with truncation and coefficient simplification.
Object display methods
python/cuopt/cuopt/linear_programming/problem.py
Variable, LinearExpression, QuadraticExpression, and Constraint render readable string and repr forms; constraint initialization retains the original expression so display output can reflect its structure and adjusted right-hand side.
Problem summary display
python/cuopt/cuopt/linear_programming/problem.py
Problem.__repr__ and Problem.__str__ produce a multi-line summary with model name, objective sense, variable counts by type, linear and quadratic constraint counts, nonzero count, and solved-state status and objective value.
Display formatting tests
python/cuopt/cuopt/tests/linear_programming/test_python_API.py
Tests cover exact __str__ and __repr__ output for LP objects, unnamed and solved-state cases, and truncation behavior for large linear and quadratic expressions at and beyond the term cap.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding algebraic str and detailed repr support to Python LP API classes.
Description check ✅ Passed The description clearly matches the implemented string and repr additions for the Python LP modeling classes and their tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/cuopt/cuopt/tests/linear_programming/test_python_API.py (1)

14-25: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add missing LinearExpression import to prevent NameError in the new test.

LinearExpression is referenced at Line 873, Line 875, and Line 876 but is not imported, so this test will fail at runtime.

Proposed fix
 from cuopt.linear_programming.problem import (
     CONTINUOUS,
     INTEGER,
     MAXIMIZE,
     MINIMIZE,
     SEMI_CONTINUOUS,
     CType,
+    LinearExpression,
     Problem,
     VType,
     sense,
     QuadraticExpression,
 )
As per coding guidelines, “Run pre-commit hooks before committing code to enforce code linters and formatters”; Ruff’s F821 here indicates a correctness break that should be fixed before merge.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuopt/cuopt/tests/linear_programming/test_python_API.py` around lines
14 - 25, The test imports from cuopt.linear_programming.problem but omits
LinearExpression, causing NameError in tests referencing LinearExpression;
update the import list in test_python_API.py to include LinearExpression (i.e.,
add LinearExpression to the grouped import alongside CONTINUOUS, INTEGER,
MAXIMIZE, MINIMIZE, SEMI_CONTINUOUS, CType, Problem, VType, sense,
QuadraticExpression) so the symbol is available where referenced.

Sources: Coding guidelines, Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/cuopt/cuopt/linear_programming/problem.py`:
- Line 21: The _SENSE_SYMBOLS dictionary is created before the constants LE, GE,
and EQ are defined, causing an import-time NameError; fix by deferring its
construction until after those constants are declared (or by keying it with the
raw numeric/char codes used for LE/GE/EQ instead of the names), i.e., move or
rebuild _SENSE_SYMBOLS after the definitions of LE, GE, EQ (or replace keys with
the literal codes) so references in _SENSE_SYMBOLS resolve correctly.

---

Outside diff comments:
In `@python/cuopt/cuopt/tests/linear_programming/test_python_API.py`:
- Around line 14-25: The test imports from cuopt.linear_programming.problem but
omits LinearExpression, causing NameError in tests referencing LinearExpression;
update the import list in test_python_API.py to include LinearExpression (i.e.,
add LinearExpression to the grouped import alongside CONTINUOUS, INTEGER,
MAXIMIZE, MINIMIZE, SEMI_CONTINUOUS, CType, Problem, VType, sense,
QuadraticExpression) so the symbol is available where referenced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 84099242-4510-46e2-b7dc-41322ef3afa6

📥 Commits

Reviewing files that changed from the base of the PR and between 2384454 and 3a01e57.

📒 Files selected for processing (2)
  • python/cuopt/cuopt/linear_programming/problem.py
  • python/cuopt/cuopt/tests/linear_programming/test_python_API.py

Comment thread python/cuopt/cuopt/linear_programming/problem.py Outdated
@chris-maes

Copy link
Copy Markdown
Contributor

This is interesting. Could you provide some example output, showing what strings this would compute, on a few different problems?

@jackthepunished

jackthepunished commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Here are some examples of what you'd see when printing or repr'ing objects after this change. On a small MILP with constraints like 2x + 3y <= 10, x - y >= 0, and x + 1 == 5, you get readable algebra instead of memory addresses: str(c1) gives 2.0 * x + 3.0 * y <= 10.0, str(c2) gives x - y >= 0.0, and str(c3) gives x + 1.0 == 5.0 (the last one keeps the constant on the LHS as the user wrote it, not the normalized internal form). Variables print as their name (str(x) → x) or as C{index} when unnamed (str(z) → C2), and repr(x) gives something like <cuopt.Variable 'x' (index=0), type=CONTINUOUS, bounds=[0.0, 10.0], value=nan>. For a QP, quadratic terms format naturally: str(xx) → x^2, str(xx + 2xy + 3x) → x^2 + 2.0 * x * y + 3.0 * x, and mixed-sign expressions like -xx + 0.5yy + x*y → -x^2 + 0.5 * y^2 + x * y. On the problem itself, str(prob) is a short summary with the problem name, objective sense, variable/constraint counts, non-zero count, and after solve the status and objective value, while repr(prob) stays compact, e.g. <cuopt.Problem 'str_repr_test' (3 vars, 3 constrs, IsMIP=True)>.

@jackthepunished

Copy link
Copy Markdown
Contributor Author

@chris-maes can you /review

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

@chris-maes May I get your review on this PR

@mlubin mlubin requested review from mlubin and removed request for chris-maes June 25, 2026 19:46
@mlubin

mlubin commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

I think this would be a nice addition. My main concern on the behavior side is there's no truncation in the case of large expressions. I doubt users want to see a linear or quadratic expression with 10,000 terms in it. If we can work out the right behavior in this case and validate it in the both REPL and notebooks then we'll be on track to merging this.

@mlubin mlubin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting the changes I mentioned above.

Linear and quadratic expressions now render only the first
_MAX_DISPLAY_TERMS (10) terms followed by a "... (N more terms)" marker,
so printing a model with thousands of terms stays readable in a REPL or
notebook instead of flooding the output. Applies to both __str__ and
__repr__ (and therefore Constraint, whose LHS is the expression); the
cap is a module constant and can be set to None to disable.

Also fix an import-time NameError: the module-level _SENSE_SYMBOLS table
referenced LE/GE/EQ before they were defined. Re-key it by the
underlying CType char codes ("L"/"G"/"E"), which CType members compare
and hash equal to, so the module imports regardless of definition order.

Add test_str_truncation_large_expression covering the linear/quadratic
head + marker, the exactly-at-cap (no marker) and one-over (singular)
boundaries, and the truncation-disabled case.

Signed-off-by: jackthepunished <kosapinarbahadir@gmail.com>
@jackthepunished jackthepunished force-pushed the feature/str-repr-for-lp-api branch from aca0220 to cd6c5ca Compare June 26, 2026 23:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/cuopt/cuopt/tests/linear_programming/test_python_API.py`:
- Around line 991-998: The quadratic truncation test is too loose because it
only asserts an upper bound on the rendered term count, so regressions that
display fewer than the intended display limit could still pass. Tighten the
assertions in the QuadraticExpression string/representation test to match the
same exact truncation boundary used by the linear expression branch, using
qexpr/qs and the existing _MAX_DISPLAY_TERMS behavior to verify the precise
number of displayed quadratic terms and the expected ellipsis suffix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 99a0419f-4f28-4154-833f-85c8de41d7dc

📥 Commits

Reviewing files that changed from the base of the PR and between 3a01e57 and aca0220.

📒 Files selected for processing (2)
  • python/cuopt/cuopt/linear_programming/problem.py
  • python/cuopt/cuopt/tests/linear_programming/test_python_API.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/cuopt/cuopt/linear_programming/problem.py

Comment on lines +991 to +998
# === QuadraticExpression also truncates ===
qexpr = xs[0] * xs[0]
for i in range(1, n):
qexpr = qexpr + xs[i] * xs[i]
qs = str(qexpr)
assert qs.count("^2") <= cap
assert qs.endswith(f"... ({n - cap} more terms)")
assert repr(qexpr) == f"<cuopt.QuadraticExpression: {qs}>"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tighten the quadratic truncation assertion.

Line 996 only checks <= cap, so a regression that renders fewer than _MAX_DISPLAY_TERMS quadratic terms would still pass. This test should pin the exact truncation boundary the same way the linear branch does. As per path instructions, "Focus review signal on correctness/API behavior for the new __str__/__repr__ outputs and their truncation/tests."

Suggested test tightening
-    assert qs.count("^2") <= cap
+    assert qs.count("^2") == cap
+    assert qs.startswith("x0^2 + ")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# === QuadraticExpression also truncates ===
qexpr = xs[0] * xs[0]
for i in range(1, n):
qexpr = qexpr + xs[i] * xs[i]
qs = str(qexpr)
assert qs.count("^2") <= cap
assert qs.endswith(f"... ({n - cap} more terms)")
assert repr(qexpr) == f"<cuopt.QuadraticExpression: {qs}>"
# === QuadraticExpression also truncates ===
qexpr = xs[0] * xs[0]
for i in range(1, n):
qexpr = qexpr + xs[i] * xs[i]
qs = str(qexpr)
assert qs.count("^2") == cap
assert qs.startswith("x0^2 + ")
assert qs.endswith(f"... ({n - cap} more terms)")
assert repr(qexpr) == f"<cuopt.QuadraticExpression: {qs}>"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuopt/cuopt/tests/linear_programming/test_python_API.py` around lines
991 - 998, The quadratic truncation test is too loose because it only asserts an
upper bound on the rendered term count, so regressions that display fewer than
the intended display limit could still pass. Tighten the assertions in the
QuadraticExpression string/representation test to match the same exact
truncation boundary used by the linear expression branch, using qexpr/qs and the
existing _MAX_DISPLAY_TERMS behavior to verify the precise number of displayed
quadratic terms and the expected ellipsis suffix.

Source: Path instructions

@mlubin mlubin added non-breaking Introduces a non-breaking change improvement Improves an existing functionality labels Jun 26, 2026
@jackthepunished jackthepunished requested a review from mlubin June 27, 2026 20:30
name = var.VariableName
if name:
return name
if getattr(var, "index", -1) >= 0:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why getattr instead of var.getVariableIndex()? When would the attribute not exist? And when would it be -1?

@@ -16,6 +16,133 @@
import warnings


# ---- Display helpers for __str__/__repr__ ----

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The location of this code isn't very natural. Why are we defining the printing helpers before VType, CType, Variable, etc?

# string. Using the codes (rather than the LE/GE/EQ aliases) also keeps this
# module-level table independent of definition order.
_SENSE_SYMBOLS = {"L": "<=", "G": ">=", "E": "=="}
_TYPE_NAMES = {"C": "CONTINUOUS", "I": "INTEGER", "S": "SEMI_CONTINUOUS"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use VType().name rather than hardcoding this mapping.

# Keyed by the underlying CType char codes ("L"/"G"/"E"). CType is a
# ``(str, Enum)`` whose members compare and hash equal to these codes, so the
# lookup works whether ``Constraint.Sense`` holds a CType member or a raw
# string. Using the codes (rather than the LE/GE/EQ aliases) also keeps this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this mapping be a member of the CType enum?


def __repr__(self):
name = _var_display_name(self)
idx = getattr(self, "index", -1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again why getattr?

@@ -1322,6 +1492,7 @@ def __init__(self, expr, sense, rhs, name=""):
self.ConstraintName = name
self.DualValue = float("nan")
self.Slack = float("nan")
self._expr = expr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we're potentially doubling the model's memory usage by storing an extra copy of the constraint data here. That's not ideal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants