Skip to content

Commit b113780

Browse files
authored
Improve and standardize the cuPDLPx Python API (#100)
* Fix: prevent lp_problem/result leak on exception in solve_once * Fix: map FEAS_POLISH_SUCCESS status code and add missing constants * Fix: avoid mutating caller's matrix and support scipy sparse arrays * Fix: validate param names and fix setWarmStart semantics * Cleanup: drop dead code in optimize() and fix pytest config * Feat: make Status an int code * Docs: align comment/docstring style * Fix: validate sparse matrix structure in solve_once * Feat: expose model data as validated properties * Fix: update warm start test * Docs: fix stale warm start and default-bound descriptions * Docs: clean comments * Fix: drop test use of nonexistent InfeasibleTol param * Test: add API-surface coverage and ignore coverage artifacts * Feat: model setter validation * Fix: expose documented Python API exports * Fix: validate solver result buffers * Feat: validate Python solver params * Fix: prevent in-place model data mutation * Feat: add Python param reset helper * Fix: disable presolve in limit test * Fix: validate core solver params * Fix: validate core model data * Fix: reject NaN model bounds * Feat: return model from optimize * Docs: update Python API docs * Feat: extend Params mapping helpers * Fix: accept numpy scalars and 0/1 for solver params * Fix: guard param validation and init ordering
1 parent 3a6e310 commit b113780

19 files changed

Lines changed: 1214 additions & 251 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,18 @@ dkms.conf
5858
# ignored files
5959
build/*
6060
test/*
61+
!test/*.py
6162
/.vscode
6263
/.venv
6364
/_b
6465
*.whl
6566
*.pyc
6667

68+
# coverage / pytest artifacts
69+
.coverage
70+
coverage.xml
71+
htmlcov/
72+
.pytest_cache/
6773

6874
*.txt
6975
*.sh

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ test = [
5252
]
5353

5454
[tool.pytest.ini_options]
55-
testpaths = ["tests"]
55+
testpaths = ["test"]
5656
addopts = """
5757
-q
5858
-ra

pytest.ini

Lines changed: 0 additions & 2 deletions
This file was deleted.

python/README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ m.setParams(OutputFlag=True, OptimalityTol=1e-8)
7575
m.optimize()
7676

7777
# Retrieve results
78-
print("Status:", m.Status)
78+
print("Status:", m.StatusName)
7979
print("Objective:", m.ObjVal)
8080
print("Primal solution:", m.X)
8181
print("Dual solution:", m.Pi)
@@ -98,7 +98,7 @@ $$
9898
- **constraint_matrix** (`A`): Coefficient matrix for the constraints. Both dense (`numpy.ndarray`) and sparse (`scipy.sparse.csr_matrix`) inputs are supported. Internally stored in double precision (`float64`).
9999
- **constraint_lower_bound** (`l`): Lower bounds for each constraint. Use `-np.inf` or `None` for no lower bound.
100100
- **constraint_upper_bound** (`u`): Upper bounds for each constraint. Use `+np.inf` or `None` for no upper bound.
101-
- **variable_lower_bound** (`lb`, optional): Lower bounds for the decision variables. Defaults to `0` for all variables if not provided.
101+
- **variable_lower_bound** (`lb`, optional): Lower bounds for the decision variables. Defaults to `-np.inf` for all variables if not provided.
102102
- **variable_upper_bound** (`ub`, optional): Upper bounds for the decision variables. Defaults to `+np.inf` for all variables if not provided.
103103
- **objective_constant** (`c0`, optional): Constant offset in the objective function. Defaults to `0.0`.
104104

@@ -125,7 +125,7 @@ m = Model(objective_vector=c,
125125

126126
### Reading from MPS Files
127127

128-
A `Model` can also be created directly from an MPS file (plain or gzip-compressed) with `cupdlpx.read`, similar to `gurobipy.read`:
128+
A `Model` can also be created directly from an MPS file (plain or gzip-compressed) with `cupdlpx.read`:
129129

130130
```python
131131
import cupdlpx
@@ -174,7 +174,7 @@ Below is a list of commonly used parameters, their internal keys, and descriptio
174174
| `ReflectionCoeff` | `reflection_coefficient` | float | `1.0` | Reflection coefficient. |
175175
| `SVMaxIter` | `sv_max_iter` | int | 5000 | Maximum number of iterations for the power method |
176176
| `SVTol`| `sv_tol` | float | `1e-4` | Termination tolerance for the power method |
177-
| `Presolve`| `presolve` | float | `True` | Whether to use presolve. |
177+
| `Presolve`| `presolve` | bool | `True` | Whether to use presolve. |
178178
| `FeasibilityPolishing` | `feasibility_polishing` | bool | `False` | Run feasibility polishing process.|
179179
| `FeasibilityPolishingTol` | `eps_feas_polish_relative` | float | `1e-6` | Relative tolerance for primal/dual residual. |
180180

@@ -191,18 +191,21 @@ m.setParams(TimeLimit=300, FeasibilityTol=1e-6)
191191
# Method 3: attribute-style access
192192
m.Params.TimeLimit = 300
193193
m.Params.FeasibilityTol = 1e-6
194+
195+
# Reset all parameters to backend defaults
196+
m.resetParams()
194197
```
195198

196199
## Solution Attributes
197200

198-
After calling `m.optimize()`, the solver stores results in a set of read-only attributes. These attributes provide access to primal/dual solutions, objective values, residuals, and runtime statistics.
201+
After calling `m.optimize()`, the solver stores results in a set of read-only attributes. `optimize()` returns the model itself, so chained access like `m.optimize().Status` is also supported. These attributes provide access to primal/dual solutions, objective values, residuals, and runtime statistics.
199202

200203
### Attribute Reference
201204

202205
| Attribute | Type | Description |
203206
|---|---|---|
204-
| `Status` | str | Human-readable solver status (`"OPTIMAL"`, `"INFEASIBLE"`, `"UNBOUNDED"`, `"TIME_LIMIT"`, etc.). |
205-
| `StatusCode` | int | Numeric status code (`OPTIMAL=1`, `INFEASIBLE=2`, `UNBOUNDED=3`, `ITERATION_LIMIT=4`, `TIME_LIMIT=5`, `UNSPECIFIED=-1`). |
207+
| `Status` | int | Integer termination status code; compare against `cupdlpx.PDLP` constants: `OPTIMAL=0`, `PRIMAL_INFEASIBLE=1`, `DUAL_INFEASIBLE=2`, `TIME_LIMIT=3`, `ITERATION_LIMIT=4`, `INFEASIBLE_OR_UNBOUNDED=5`, `FEAS_POLISH_SUCCESS=6`, `UNSPECIFIED=-1`. |
208+
| `StatusName` | str | Human-readable status name, e.g. `"OPTIMAL"`, `"PRIMAL_INFEASIBLE"`. |
206209
| `ObjVal` | float | Primal objective value at termination (sign-adjusted according to `ModelSense`). |
207210
| `DualObj` | float | Dual objective value at termination. |
208211
| `Gap` | float | Absolute primal-dual gap. |
@@ -225,7 +228,7 @@ All solution-related information can then be queried directly from the `Model` o
225228
```python
226229
m.optimize()
227230

228-
print("Status:", m.Status, "(code:", m.StatusCode, ")")
231+
print("Status:", m.StatusName, "(code:", m.Status, ")")
229232
print("Primal objective:", m.ObjVal)
230233
print("Dual objective:", m.DualObj)
231234
print("Relative gap:", m.RelGap)
@@ -268,7 +271,7 @@ m.setWarmStart(primal=x_init)
268271
m.setWarmStart(dual=pi_init)
269272
```
270273

271-
If the warm-start vectors have incorrect dimensions, the solver automatically falls back to a cold start and issues a warning.
274+
If the warm-start vectors have incorrect dimensions, `setWarmStart` raises a `ValueError`. Omitting an argument leaves that side unchanged; passing `None` clears it.
272275

273276
To clear existing warm-start values:
274277

python/cupdlpx/PDLP.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
# Objective
15+
"""Objective-sense and termination-status constants, plus parameter-name aliases."""
16+
17+
# Objective
1618
MINIMIZE = 1
1719
MAXIMIZE = -1
1820

19-
# Status codes
20-
OPTIMAL = 0
21-
PRIMAL_INFEASIBLE = 1
22-
DUAL_INFEASIBLE = 2
23-
TIME_LIMIT = 3
24-
ITERATION_LIMIT = 4
25-
UNSPECIFIED = -1
21+
# Status codes (must match status_to_code in python_bindings/_core_bindings.cpp)
22+
OPTIMAL = 0
23+
PRIMAL_INFEASIBLE = 1
24+
DUAL_INFEASIBLE = 2
25+
TIME_LIMIT = 3
26+
ITERATION_LIMIT = 4
27+
INFEASIBLE_OR_UNBOUNDED = 5
28+
FEAS_POLISH_SUCCESS = 6
29+
UNSPECIFIED = -1
2630

2731

2832
# parameter name alias

python/cupdlpx/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
"""cuPDLPx: Python bindings for the GPU-accelerated first-order LP solver."""
16+
1517
import os
1618
import platform
1719

1820
# Windows only: register CUDA bin for dependent DLL loading.
19-
if platform.system() == "Windows":
21+
if platform.system() == "Windows": # pragma: no cover
2022
cuda_path = os.environ.get("CUDA_PATH")
2123
if cuda_path:
2224
bin_path = os.path.join(cuda_path, "bin")
@@ -26,12 +28,12 @@
2628
from .model import Model, read
2729
from . import PDLP
2830

29-
__all__ = ["Model", "read"]
30-
3131
# versioning
3232
from importlib.metadata import version, PackageNotFoundError
3333
# get version from package metadata (toml file)
3434
try:
3535
__version__ = version("cupdlpx")
36-
except PackageNotFoundError:
36+
except PackageNotFoundError: # pragma: no cover
3737
__version__ = "0.0.0"
38+
39+
__all__ = ["Model", "PDLP", "read", "__version__"]

python/cupdlpx/_core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
"""Thin re-export of the compiled cuPDLPx core extension (_cupdlpx_core)."""
16+
1517
from ._cupdlpx_core import solve_once, get_default_params, read_mps

0 commit comments

Comments
 (0)