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
234 changes: 233 additions & 1 deletion python/cuopt/cuopt/linear_programming/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ class CType(str, Enum):
GE = "G"
EQ = "E"

@property
def symbol(self):
"""Algebraic symbol used when printing constraints."""
return {CType.LE: "<=", CType.GE: ">=", CType.EQ: "=="}[self]

Comment on lines +51 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A4 -B2 \
  'def symbol|def __str__|def __repr__' \
  python/cuopt/cuopt/linear_programming/problem.py

Repository: NVIDIA/cuopt

Length of output: 2928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=python/cuopt/cuopt/linear_programming/problem.py

printf '%s\n' '--- targeted diff ---'
git diff --unified=12 -- "$file" | sed -n \
  '/CType/,/^[^+ -].*$/p' | head -n 220

printf '%s\n' '--- class and method definitions ---'
rg -n '^(class (CType|Variable|Constraint)|    def (symbol|__str__|__repr__))' "$file"

printf '%s\n' '--- surrounding source ---'
sed -n '35,65p;315,375p;1535,1590p' "$file"

Repository: NVIDIA/cuopt

Length of output: 5668


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path

path = Path("python/cuopt/cuopt/linear_programming/problem.py")
tree = ast.parse(path.read_text())

wanted = {"CType", "Variable", "Constraint"}
for node in tree.body:
    if isinstance(node, ast.ClassDef) and node.name in wanted:
        print(f"class {node.name}:")
        for item in node.body:
            if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
                if item.name in {"symbol", "__str__", "__repr__"}:
                    returns = ast.unparse(item.returns) if item.returns else "<missing>"
                    doc = ast.get_docstring(item, clean=False)
                    print(
                        f"  {item.name}: return={returns}; "
                        f"docstring={'yes' if doc else 'no'}"
                    )
                    if doc:
                        print(f"    {doc!r}")
PY

Repository: NVIDIA/cuopt

Length of output: 415


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=python/cuopt/cuopt/linear_programming/problem.py

printf '%s\n' '--- repository state ---'
git status --short
git log -1 --oneline
git diff --stat
git show --stat --oneline HEAD

printf '%s\n' '--- commit-level changes in target file ---'
git show --format= --unified=3 HEAD -- "$file" | sed -n '1,260p'

printf '%s\n' '--- method blocks and immediate docstrings ---'
python3 - "$file" <<'PY'
from pathlib import Path
import re
import sys

text = Path(sys.argv[1]).read_text().splitlines()
targets = {"symbol", "__str__", "__repr__"}
class_name = None
for i, line in enumerate(text):
    class_match = re.match(r"class\s+([A-Za-z_]\w*)\b", line)
    if class_match:
        class_name = class_match.group(1)
    method_match = re.match(r"    def\s+([A-Za-z_]\w*)\s*\((.*?)\)\s*(?:->\s*([^:]+))?:", line)
    if method_match and method_match.group(1) in targets:
        name, args, ret = method_match.groups()
        print(f"{class_name}.{name}: return={ret or '<missing>'}, line={i + 1}")
        j = i + 1
        while j < len(text) and text[j].strip() == "":
            j += 1
        if j < len(text) and text[j].lstrip().startswith(('"""', "'''")):
            print(f"  docstring starts line {j + 1}: {text[j].strip()}")
        else:
            print("  docstring: <missing>")
PY

Repository: NVIDIA/cuopt

Length of output: 11182


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=python/cuopt/cuopt/linear_programming/problem.py

printf '%s\n' '--- parent definitions ---'
git show HEAD^:"$file" | sed -n '35,65p;435,485p;1510,1570p'

printf '%s\n' '--- exact changed definitions ---'
git diff --unified=0 HEAD^ HEAD -- "$file" | rg -n -A18 -B4 \
  'symbol|def __str__|def __repr__|^@@'

printf '%s\n' '--- annotations and Returns sections in public APIs ---'
rg -n -U \
  '^[ ]{4}(def|async def) [A-Za-z_][A-Za-z0-9_]*\([^\\n]*\)( -> [^:]+)?:\\n|^[ ]{4}.*Returns?:' \
  "$file" | head -n 160

Repository: NVIDIA/cuopt

Length of output: 10805


Annotate the new CType.symbol property.

Add -> str and document the returned algebraic constraint symbol. The other display methods already existed.

📍 Affects 1 file
  • python/cuopt/cuopt/linear_programming/problem.py#L51-L55 (this comment)
  • python/cuopt/cuopt/linear_programming/problem.py#L343-L359
  • python/cuopt/cuopt/linear_programming/problem.py#L1559-L1579
🤖 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/linear_programming/problem.py` around lines 51 - 55,
Update the CType.symbol property in
python/cuopt/cuopt/linear_programming/problem.py at lines 51-55 to add a str
return annotation and document that it returns the algebraic constraint symbol;
apply the same annotation/documentation change to the sibling display methods at
lines 343-359 and 1559-1579 as requested.

Sources: Coding guidelines, Path instructions


LE = CType.LE
GE = CType.GE
Expand Down Expand Up @@ -335,6 +340,123 @@ def __eq__(self, other):
case _:
raise ValueError("Unsupported operation")

def __str__(self):
if self.VariableName:
return self.VariableName
if self.index >= 0:
return f"C{self.index}"
# Not yet added to a problem: no name and no index to show.
return f"V{id(self)}"

def __repr__(self):
vtype = self.VariableType
if isinstance(vtype, (bytes, bytearray)):
# The MPS data model yields variable types as byte codes.
vtype = vtype.decode()
return (
f"<cuopt.Variable '{self}' (index={self.index}), "
f"type={VType(vtype).name}, bounds=[{self.LB}, {self.UB}], "
f"value={self.Value}>"
)


# Maximum number of terms rendered when stringifying a linear or quadratic
# expression. Beyond this, the head is shown followed by a ``... (N more
# terms)`` marker so that printing a model with thousands of terms stays
# readable in a REPL or notebook instead of flooding the output. Set to
# ``None`` to disable truncation entirely.
_MAX_DISPLAY_TERMS = 10


class _ExprBuilder:
"""Build an algebraic string from a sequence of terms.

The first term is emitted without a sign; subsequent terms are joined
with ' + ' or ' - ' separators. A coefficient of 1.0 or -1.0 is
elided, so '1.0 * x' becomes 'x' and '-1.0 * x' becomes '-x'.

When ``max_terms`` is set, only the first ``max_terms`` non-zero terms
are rendered; any remaining terms are counted and summarized as a
trailing ``... (N more terms)`` marker. This keeps the output bounded
for expressions with very many terms. ``max_terms=None`` (the default)
renders every term.
"""

def __init__(self, max_terms=None):
self.parts = []
self.max_terms = max_terms
# Non-zero terms seen so far (rendered + hidden).
self.n_terms = 0
# Non-zero terms omitted because the cap was reached.
self.n_hidden = 0

def add_linear(self, coef, var):
"""Add a linear term ``coef * var``."""
if coef == 0.0:
return
var_str = str(var)
if coef == 1.0:
self._append(var_str, negative=False)
elif coef == -1.0:
self._append(var_str, negative=True)
else:
self._append(f"{abs(coef)} * {var_str}", negative=coef < 0)

def add_quadratic(self, coef, var1, var2):
"""Add a quadratic term ``coef * var1 * var2``."""
if coef == 0.0:
return
v1_str = str(var1)
v2_str = str(var2)
if v1_str == v2_str:
term_str = f"{v1_str}^2"
elif v1_str <= v2_str:
term_str = f"{v1_str} * {v2_str}"
else:
term_str = f"{v2_str} * {v1_str}"
Comment on lines +409 to +416
if coef == 1.0:
self._append(term_str, negative=False)
elif coef == -1.0:
self._append(term_str, negative=True)
else:
self._append(f"{abs(coef)} * {term_str}", negative=coef < 0)

def add_constant(self, value):
"""Add a constant term."""
if value == 0.0:
return
self._append(f"{abs(value)}", negative=value < 0)

def _append(self, term, negative):
self.n_terms += 1
if self.max_terms is not None and self.n_terms > self.max_terms:
# Past the cap: count the term but don't render it.
self.n_hidden += 1
return
if not self.parts:
self.parts.append(f"-{term}" if negative else term)
else:
self.parts.append(f" - {term}" if negative else f" + {term}")

def build(self):
if not self.parts and not self.n_hidden:
return "0.0"
result = "".join(self.parts)
if self.n_hidden:
plural = "term" if self.n_hidden == 1 else "terms"
marker = f"... ({self.n_hidden} more {plural})"
result = f"{result} + {marker}" if result else marker
return result


def _format_linear(vars, coeffs, constant, max_terms=None):
"""Format a linear expression as an algebraic string."""
builder = _ExprBuilder(max_terms=max_terms)
for var, coef in zip(vars, coeffs):
builder.add_linear(coef, var)
builder.add_constant(constant)
return builder.build()


class QuadraticExpression:
"""
Expand Down Expand Up @@ -889,6 +1011,25 @@ def __ge__(self, other):
def __eq__(self, other):
raise ValueError("Equality constraints are not supported.")

def __str__(self):
builder = _ExprBuilder(max_terms=_MAX_DISPLAY_TERMS)
if self.qmatrix is not None:
for row, col, val in zip(
self.qmatrix.row, self.qmatrix.col, self.qmatrix.data
):
if val == 0.0:
continue
builder.add_quadratic(val, self.qvars[row], self.qvars[col])
for v1, v2, coef in zip(self.qvars1, self.qvars2, self.qcoefficients):
builder.add_quadratic(coef, v1, v2)
for var, coef in zip(self.vars, self.coefficients):
builder.add_linear(coef, var)
builder.add_constant(self.constant)
return builder.build()

def __repr__(self):
return f"<cuopt.QuadraticExpression: {self}>"


def _quadratic_expression_to_qcmatrix(expr, rhs):
"""Build QCMATRIX COO data for a quadratic row ``expr`` sense ``rhs``.
Expand Down Expand Up @@ -1280,6 +1421,17 @@ def __eq__(self, other):
expr = self - other
return Constraint(expr, EQ, 0.0)

def __str__(self):
return _format_linear(
self.vars,
self.coefficients,
self.constant,
max_terms=_MAX_DISPLAY_TERMS,
)

def __repr__(self):
return f"<cuopt.LinearExpression: {self}>"


class Constraint:
"""
Expand Down Expand Up @@ -1341,7 +1493,14 @@ def __init__(self, expr, sense, rhs, name=""):
self.rhs_value = rhs_value
self.RHS = rhs_value
self.vindex_coeff_dict = {}
self.vars = expr.vars
# All participating variables (linear and quadratic terms), so
# that QCMATRIX indices can be mapped back to variables.
# Deduplicated by identity: Variable overloads __eq__ to build
# constraints, so instances are unhashable.
seen = {}
for var in (*expr.vars, *expr.qvars1, *expr.qvars2, *expr.qvars):
seen.setdefault(id(var), var)
self.vars = list(seen.values())
return

self.is_quadratic = False
Expand Down Expand Up @@ -1397,6 +1556,28 @@ def compute_slack(self):

return self.RHS - lhs

def __str__(self):
# Rendered from the data the constraint stores for the solver, so
# the output is normalized: duplicate terms are merged and any
# expression constant is folded into the right-hand side.
builder = _ExprBuilder(max_terms=_MAX_DISPLAY_TERMS)
index_to_var = {v.index: v for v in self.vars}
if self.is_quadratic:
for row, col, val in zip(self.rows, self.cols, self.vals):
builder.add_quadratic(
val, index_to_var[row], index_to_var[col]
)
for idx, val in zip(self.linear_indices, self.linear_values):
builder.add_linear(val, index_to_var[idx])
else:
for idx, coeff in self.vindex_coeff_dict.items():
builder.add_linear(coeff, index_to_var[idx])
Comment on lines +1564 to +1574
return f"{builder.build()} {CType(self.Sense).symbol} {self.RHS}"

def __repr__(self):
name = self.ConstraintName if self.ConstraintName else "<unnamed>"
return f"<cuopt.Constraint '{name}': {self}>"


class Problem:
"""
Expand Down Expand Up @@ -2219,3 +2400,54 @@ def solve(self, settings=solver_settings.SolverSettings()):
# Post Solve
self.populate_solution(solution)
return solution

def __repr__(self):
name = self.Name if self.Name else "<unnamed>"
return (
f"<cuopt.Problem '{name}' "
f"({len(self.vars)} vars, {len(self.constrs)} constrs, "
f"IsMIP={self.IsMIP})>"
)

def __str__(self):
lines = []
name = self.Name if self.Name else "<unnamed>"
lines.append(f"Problem: {name}")
sense_str = "MINIMIZE" if self.ObjSense == MINIMIZE else "MAXIMIZE"
lines.append(f" Objective: {sense_str}")

n_cont = 0
n_int = 0
n_semi = 0
for v in self.vars:
t = v.VariableType
if isinstance(t, (bytes, bytearray)):
t = t.decode()
if t in ("I", VType.INTEGER):
n_int += 1
elif t in ("S", VType.SEMI_CONTINUOUS):
n_semi += 1
else:
n_cont += 1
lines.append(
f" Variables: {len(self.vars)} "
f"(continuous={n_cont}, integer={n_int}, "
f"semi-continuous={n_semi})"
)

n_linear = sum(1 for c in self.constrs if not c.is_quadratic)
n_quad = sum(1 for c in self.constrs if c.is_quadratic)
lines.append(
f" Constraints: {len(self.constrs)} "
f"(linear={n_linear}, quadratic={n_quad})"
)
lines.append(f" Non-zeros: {self.NumNZs}")

if self.solved:
status = self.Status
if hasattr(status, "name"):
status = status.name
lines.append(f" Status: {status}")
lines.append(f" Objective value: {self.ObjValue}")

return "\n".join(lines)
Loading