Skip to content

Java bindings for LP, MIP and QP - #1524

Open
nvidiacbrissette wants to merge 38 commits into
NVIDIA:mainfrom
nvidiacbrissette:cbrissette/cuopt-bindings
Open

Java bindings for LP, MIP and QP#1524
nvidiacbrissette wants to merge 38 commits into
NVIDIA:mainfrom
nvidiacbrissette:cbrissette/cuopt-bindings

Conversation

@nvidiacbrissette

@nvidiacbrissette nvidiacbrissette commented Jul 7, 2026

Copy link
Copy Markdown

Description

Java bindings for LP, MIP and QP, built as hand-written JNI over the public C API, with build and CI integration. Contributes to #1535 and #860.

The API is deliberately small. Following review, it covers building a problem, solving it, and reading the result — nothing beyond that. Anything further can be added later with a case for it.

API

Problem builds and solves; Variable and Constraint carry the model and the values from the last solve; SolverSettings configures the solver; Solution reports the outcome.

try (Problem problem = new Problem("burglar")) {
  Variable x = problem.addVariable(0.0, 1.0, 15.0, VariableType.INTEGER, "take_item_0");
  Variable y = problem.addVariable(0.0, 1.0, 100.0, VariableType.INTEGER, "take_item_1");
  problem.addConstraint(LinearExpression.of(x, 2).plus(y, 20).le(102.0), "capacity");
  problem.setObjective(LinearExpression.of(x, 15).plus(y, 100), ObjectiveSense.MAXIMIZE);

  try (SolverSettings settings = new SolverSettings().setSetting(CuOptConstants.CUOPT_TIME_LIMIT, 10.0);
       Solution solution = problem.solve(settings)) {
    System.out.println(solution.getTerminationStatus());
    System.out.println(x.getValue());
  }
}

Solution values are read from the model — Variable.getValue, Variable.getReducedCost, Constraint.getDualValue, Constraint.getSlack — rather than as bulk arrays.

Solver statistics are read as scalar solution attributes, keyed by a CuOptConstants.CUOPT_SOLUTION_ATTR_* value:

double gap   = solution.getFloatAttribute(CuOptConstants.CUOPT_SOLUTION_ATTR_LP_GAP);
int    nodes = solution.getIntAttribute(CuOptConstants.CUOPT_SOLUTION_ATTR_MIP_NUM_NODES);

This mirrors the problem attribute accessors, following the model @chris-maes asked for and @tmckayus clarified. Because the selectors are generated into CuOptConstants from constants.h, a statistic added later is a new constant rather than new Java. A selector that does not apply to the solution, or that does not have the requested value type, raises CuOptException.

Build integration

java is an opt-in target of the top-level build.sh:

./build.sh libcuopt                 # once
./build.sh java                     # build libcuopt_jni.so and package the jar
./build.sh java --run-java-tests    # the same, then run the suite

It prefers the cpp/build tree so it works without --install, and falls back to the conda prefix, which is what CI uses. ci/build_java.sh calls build.sh rather than duplicating the invocation.

The rmm and raft headers must be the ones libcuopt was compiled against — rmm carries its version in an inline namespace, so a mismatched copy links cleanly and then fails at dlopen. build.sh passes the right include and library paths when it targets a build tree.

CI

Workflow Job Runner Script
pr.yaml java-build gpu-l4-latest-1 ci/test_java.sh
build.yaml java-build cpu4 ci/build_java.sh
test.yaml conda-java-tests gpu-l4-latest-1 ci/test_java.sh

All three are in their workflow's aggregator job, so a Java failure fails the PR. The PR job is gated on the test_java changed-files group (java/**, ci/build_java.sh, ci/test_java.sh) and on test_cpp, so a C++ change that could break the bindings still exercises them. target/ is uploaded as the cuopt-java artifact.

The java dependency file-key pulls libcuopt plus the libraft-headers, librmm and rapids-logger headers that libcuopt's public headers include transitively. ci/release/update-version.sh bumps the POM through a sentinel comment; the artifact version drops the zero-padded RAPIDS patch field, since Maven has no notion of it.

JNI symbol check

The bindings are hand-written, so a static native declaration and its entry point can drift apart. JNI resolves lazily, so the library still loads and the failure appears only when something calls the method.

scripts/check_jni_symbols.sh diffs the prototypes javac -h derives from the Java sources against the symbols the built library exports, and fails on a mismatch either way. It reads the built artifact rather than parsing source, so the macro-generated entry points need no special casing. build_native.sh runs it after every native build, which covers ./build.sh java and both CI jobs. It takes about a second.

Packaging

Version 26.10.0. Sources and javadoc jars are attached, and the POM carries the url, licenses, scm and developers metadata Maven Central requires. Signing and upload are not wired up; that is tracked by the "Publishing & support" item in #1535.

Known gap

cuopt_jni.cpp still includes pdlp/cuopt_c_internal.hpp for parts of the problem path the C API does not cover — setting names, the quadratic objective and quadratic constraint getters, and the problem category. That couples libcuopt_jni.so to a specific libcuopt build rather than to a stable ABI, which matters before this ships as a binary artifact. Tracked in #1703. The related copy-out behaviour was #1706, fixed by #1734 and now in main.

Checklist

@nvidiacbrissette
nvidiacbrissette requested review from a team as code owners July 7, 2026 19:34
@nvidiacbrissette
nvidiacbrissette requested a review from tmckayus July 7, 2026 19:34
@copy-pr-bot

copy-pr-bot Bot commented Jul 7, 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.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Java bindings

Layer / File(s) Summary
Modeling contracts and expressions
java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/*
Adds Java variables, expressions, constraints, objectives, enums, callbacks, statistics, and validation behavior.
JNI bridge and native wrappers
java/cuopt/src/main/native/*, cpp/include/cuopt/mathematical_optimization/cuopt_c.h, cpp/src/pdlp/cuopt_c.cpp
Adds native declarations and JNI implementations for model creation, solving, settings, callbacks, persistence, solution fields, and statistics.
Problem modeling and solve flow
java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Problem.java
Adds model construction, MPS I/O, CSR/QCSR inspection, updates, relaxation, MIP starts, solving, and result propagation.
Build, packaging, and CI
java/cuopt/*, ci/*, build.sh, .github/workflows/*, dependencies.yaml
Adds Maven/CMake builds, native scripts, generated constants, Java dependencies, CI jobs, artifacts, and release version handling.
Validation and documentation
java/cuopt/src/test/*, docs/cuopt/source/cuopt-java/*, docs/cuopt/source/index.rst
Adds modeling and native integration tests plus Java quick-start, convex, and MIP documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: tmckayus, chris-maes, hlinsen

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.68% 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
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.
Title check ✅ Passed The title clearly summarizes the primary change: Java bindings for LP, MIP, and QP functionality.
Description check ✅ Passed The description directly explains the Java bindings, API scope, build and CI integration, testing, documentation, and known limitations.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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.

I saw the PR is closed, sending my comments as I had them already written up.

Comment thread java/cuopt/src/main/java/com/nvidia/cuopt/linearprogramming/BatchSolve.java Outdated

extern "C" {

cuopt_int_t cuOptLoadParametersFromFile(cuOptSolverSettings settings, const char* path);

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.

We should discuss merging these extensions into the C API.

@@ -0,0 +1,1367 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.

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.

I'd recommend avoiding a test dependency on the python interface. The java interface should stand on its own.

Comment thread java/cuopt/README.md Outdated
@@ -0,0 +1,25 @@
# cuOpt Java bindings (beta)

This directory is an isolated, customer-specific beta module for the cuOpt

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.

Is this how we want to ship it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We would want to follow cuvs and try to publish to maven https://mvnrepository.com/artifact/com.nvidia.cuvs/cuvs-java

Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
@nvidiacbrissette

Copy link
Copy Markdown
Author

Sorry that was an accident. Reopening.

@@ -0,0 +1,28 @@
/home/cbrissette/cuopt/java/cuopt/src/main/java/com/nvidia/cuopt/linearprogramming/BatchSolve.java

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need these files ? may be we can delete all the run time files so developers can concentrate on main parts.

@ramakrishnap-nv ramakrishnap-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Focused review on APIs and shipping (vs how cuvs ships Java).

APIs: the surface is broad and, pleasingly, closely in sync with the Python API — the algebraic Problem layer matches Python's (camelCase) modeling methods almost 1:1, and DataModel maps cleanly (snake_case→camelCase). A few parity gaps and Java-idiom nits are noted inline.

Shipping: the main blockers — don't commit target/, and wire the build into CI/release the way cuvs does (ci/build_java.sh/ci/test_java.sh, dependencies.yaml java key, workflow jobs, version marker, docs toctree).

Non-blocking review comments below.

Comment thread java/cuopt/scripts/build_native.sh
Comment thread java/cuopt/pom.xml Outdated
Comment thread docs/cuopt/source/cuopt-java/index.rst
}

/** Return true for maximize and false for minimize, matching Python get_sense(). */
public boolean getSense() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Parity note (not a rename request): getSense() correctly matches the Python DataModel.get_sense() (bool, True=maximize) — good. Two parity gaps vs Python though: (1) Python puts set_initial_primal_solution/set_initial_dual_solution on DataModel, whereas here they're on SolverSettings; (2) Python DataModel also exposes getters this class seems to lack: get_quadratic_objective_{values,indices,offsets}, get_variable_names/get_row_names, get_objective_name/get_problem_name, get_ascii_row_types.

resetSolvedValues();
}

public Object getObjective() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

getObjective() returns Object — callers must downcast. Prefer a typed return (or overloads). Same for SolverSettings.getTypedParameter() / getMipCallbacks(). (The modeling API otherwise tracks the Python Problem layer 1:1 — nice.)

Comment thread java/cuopt/src/main/java/com/nvidia/cuopt/linearprogramming/CsrMatrix.java Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-api.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-examples.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/convex-examples.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/convex/index.rst Outdated
Comment thread docs/cuopt/source/cuopt-java/index.rst Outdated
Comment thread java/cuopt/src/main/java/com/nvidia/cuopt/linearprogramming/PDLPSolverMode.java Outdated
public enum ProblemCategory {
LP(0),
MIP(1),
IP(2);

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.

We should deprecate IP across the whole code base.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed for Java.

Comment thread java/cuopt/src/main/java/com/nvidia/cuopt/linearprogramming/SolverMethod.java Outdated

@chris-maes chris-maes 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.

Thanks for adding the JAVA API. Let's make sure that capitalization is consistent before merging.

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test dc8b4b0

The public enum constant is gone, so the paragraph documented an API that no
longer exists. The only remaining mention is a comment in ProblemCategory
explaining why the native value is folded into MIP, which has to stay while the
engine still reports it.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

@chris-maes before I work through the rest of your review, I'd like to check the direction, because I think the remaining comments share one underlying question and answering it settles most of them at once.

What's already done

  • IP is gone from ProblemCategory and from the docs. The native value still has to be mapped, since the engine returns a third category when every variable is discrete (cpu_optimization_problem.cpp), so it is folded into MIP with a comment saying why.
  • readMPS / writeMPS are gone; read dispatches on the file extension and write replaces writeMPS. This also deleted the parseMPSProblem JNI entry point, which was the only remaining caller of io::read_mps.
  • getCSR / getQCSR are now getConstraintMatrix / getQuadraticObjectiveMatrix. You were right to ask what a "linear objective matrix" was — the documentation was wrong, getCSR returned the constraint matrix.
  • The five C API comments are addressed and merged separately as Expose solver statistics as scalar solution attributes #1715.

On "fluent": @mlubin noted it is standard terminology for this style of API, so I have left the wording unless you would still prefer it changed.

The question

The Java surface was built for parity with the Python API — that was its design goal, and it is why the shape looks the way it does. Your review is asking it to be consistent with the C API instead, which took the attribute direction in #1549.

Both are reasonable, but they point different ways, and five of the six items you have asked to remove exist in the Python API today:

Java Python equivalent
getProblemCategory get_problem_category — present
getPrimalSolution get_primal_solution — present
getDualSolution get_dual_solution — present
getReducedCost get_reduced_cost — present
computeSlack compute_slack — present
getObjective get_objective — present
isSolved absent

So the choice is roughly:

(a) Java aligns with the C API and diverges from Python. Java becomes the model the Python API moves toward later. Users moving between the two see different shapes until Python follows.

(b) Java matches Python for now, and both move to an attribute model together as a separate piece of work.

isSolved is the exception either way — it is not in Python and getStatus covers it, so I will remove it regardless.

What each remaining item would mean

ObjectiveExpression — it is the interface returned by getObjective, implemented only by LinearExpression and QuadraticExpression. Removing it means getObjective returns one of those two, so callers either get overloads or an instanceof check. Cheap either way; mostly a question of what you want the return type to be.

ProblemCategory — one wrinkle worth flagging. You asked why cuOptSolutionIsMIP existed when cuOptIsMIP was available. I removed it, and Solution now derives isMIP() from the problem's ProblemCategory rather than calling native. If ProblemCategory also goes, Solution needs CUOPT_ATTR_IS_MIP as its source instead. Fine either way, but the two changes interact.

getPrimalSolution / getDualSolution / getReducedCost — these already exist per-variable and per-constraint (Variable.getValue(), Variable.getReducedCost(), Constraint.getDualValue()), populated after each solve. The array forms are an additional bulk path. Removing them is straightforward; the thing lost is copying a whole solution vector out in one call, which matters for large models. Happy to remove if you would rather have one way to do it.

computeSlack in the C++ engine — agreed on the principle: if each binding computes slack itself, they can disagree on tolerances or on how quadratic constraints are handled. That is a C++ change though, so by the precedent from #1715 it should be its own PR. Can that follow separately rather than block this one?

"Attributes instead of stats" — this is the one I would most like pinned down, since it subsumes several of the above. Do you mean:

  1. a thin Java wrapper over the CUOPT_SOLUTION_ATTR_* accessors that just merged, keeping LPStats / MIPStats as the presentation; or
  2. a genuinely attribute-shaped Java API, where a caller fetches values by selector rather than through typed accessors?

Option 2 is a much larger change and would be unusual for Java, where typed accessors are the norm and an untyped selector API gives up compile-time checking. Option 1 I can do now.

Last thing

This PR is around 6,700 lines and has been open since July. Which of these do you consider blocking for merge, and which could land as follow-ups against the Java module once it is in? If most can follow, the bindings become reviewable on their own merits and the API refinement can proceed without holding up CI coverage and the build integration.

getStatus already answers the question: it stays NO_TERMINATION until a solve
populates it, so isSolved was a second way to ask something the API already
told you. This is the one item from the review that needs no decision on the
wider API direction, since the Python API has no equivalent either.

The private solved field went with it, as isSolved was its only reader.

Verified: 26/26 Java tests.
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 8b178d9

@mlubin

mlubin commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

I agree with Chris on pushing against matching Python as the design goal. I don't believe that was a goal that we specified. Keep the surface functional and minimal, we should not copy over historical cruft from the Python API.

This PR is around 6,700 lines and has been open since July. Which of these do you consider blocking for merge, and which could land as follow-ups against the Java module once it is in?

The right flow would be to trim down the PR to what's needed and unobjectionable and then add onto it later as needed.

Following mlubin: Python parity was never a stated goal, the surface should be
functional and minimal, and the right flow is to trim the PR down and add back
later as needed rather than defer removals to follow-ups.

Removed as redundant with something the API already provides:

- Solution.getProblemCategory, since isMIP answers the only distinction that
  remains once the all-integer category folds into MIP
- Solution.getSolvedBy and getSolvedByPDLP, both derivable from LPStats
- Solution.getTerminationReason, the string form of getTerminationStatus
- Solution.getVars and Problem.getIncumbentValues, wrappers over the accessors
- Problem.update, whose body was a call to resetSolvedValues
- SolverSettings' three static getSolverSetting overloads, which built and
  destroyed a native settings object per call to read a default

Removed because solve rebuilds the native problem on every call, so there is no
incremental re-solve for them to support, and setObjective with the Variable
setters already covers the same ground:

- Problem.updateConstraint and Problem.updateObjective
- Problem.relax, which duplicated model-copy logic; setting VariableType on the
  variables achieves the same thing

Narrowed rather than deleted, because the bindings themselves need them:

- Solution.getPrimalSolution, getDualSolution, getReducedCost and
  Solution.getSolveTime are package-private. Problem distributes them onto
  Variable and Constraint after each solve, so callers read values from the
  model. The bulk path is unchanged internally, so this costs no performance.
- Constraint.computeSlack is package-private. Deleting it outright would remove
  slack from Java entirely until the engine provides it, which is a separate
  C++ change.

ObjectiveExpression is gone. It existed only as a supertype for the two
expression classes, which stand on their own. getObjective now returns
LinearExpression, and the quadratic part is read as a matrix from
getQuadraticObjectiveMatrix. isQuadratic went with it from both expression
classes, where it was a constant.

Public members across the five main classes go from 109 to 90.

Verified: 24/24 Java tests, 59 JNI symbols matched, all pre-commit hooks.
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test ce50c37

They belong to NVIDIA#1734 and were swept in by a git add -A while that fix was
copied into this worktree to build against. This branch should carry only the
Java module; the C API arrives from main once NVIDIA#1734 merges.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test d1ebd46

java-build failed with:

  /opt/conda/envs/java/include/cuopt/error.hpp:14:10:
  fatal error: raft/core/error.hpp: No such file or directory

The JNI layer compiles against libcuopt's public headers, and those include
raft, rmm and rapids_logger headers transitively. The java file-key pulled in
libcuopt itself but none of those, so the conda environment CI builds in had the
headers that include them and not the headers they include.

Add the same three dependency groups py_build_libcuopt already uses.

Local builds did not catch this because they compile against the cuOpt build
tree, where build.sh passes the _deps include paths explicitly. Only the CI
path, which builds against the conda prefix, exercises this.

Verified: rapids-dependency-file-generator now resolves libraft-headers,
librmm and rapids-logger into the java environment.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 8a06bf2

rapids-bot Bot pushed a commit that referenced this pull request Aug 18, 2026
Fixes #1706.

`cuOptGetPrimalSolution`, `cuOptGetDualSolution` and `cuOptGetReducedCosts` copy into a caller-allocated buffer and report no length. When the underlying vector is empty the `memcpy` copies nothing, the function returns `CUOPT_SUCCESS`, and the caller's buffer keeps whatever it already held.

So "this solve produced no values" is indistinguishable from "the values are all zero". A caller that zeroes its buffer first reads zeros and believes them.

### Confirmed, not theoretical

Against an infeasible LP (`x >= 2` and `x <= 1`), with each buffer pre-filled with a `-12345` sentinel:

```
termination_status = 2   (INFEASIBLE)
cuOptGetPrimalSolution -> 0 (CUOPT_SUCCESS), buffer still -12345
cuOptGetDualSolution   -> 0 (CUOPT_SUCCESS), buffer still -12345
cuOptGetReducedCosts   -> 0 (CUOPT_SUCCESS), buffer still -12345
```

Note this affects **`cuOptGetPrimalSolution` as well**, which the original issue did not mention — it was found while reproducing.

This is also what forced a revert in #1524: moving the Java bindings onto `cuOptGetReducedCosts` turned "no reduced costs" into "all reduced costs are zero" for an infeasible LP, caught by `ProblemIntegrationTest.problemsBuildAndSolve[10]`. Those getters had to stay on the internal C++ interface, which does report the real length.

### The change

Return `CUOPT_INVALID_ARGUMENT` when there are no values to copy, and leave the output buffer untouched.

`CUOPT_INVALID_ARGUMENT` rather than a new status code because `cuOptGetDualSolution` **already returns it** when the underlying call throws `std::logic_error`. The empty-vector case was simply slipping past that guard, so the two paths now agree rather than one silently succeeding. Happy to use a distinct code instead if reviewers would prefer one — that would be a new public constant, which seemed like more surface than this warrants.

`cuOptGetPrimalSolution` had no exception guard at all; it now has the same one as the other two.

### Compatibility

A solve that produced values is unaffected. The behaviour only changes where the function previously reported success without writing anything.

I checked the in-tree callers: the C examples under `docs/cuopt/source/cuopt-c/` and `skills/cuopt-numerical-optimization-api/assets/c/` all branch on the status code, so they now report the failure instead of printing uninitialised memory. Python does not go through these entry points; it binds to the C++ structs via Cython.

### Tests

Two tests, covering both directions, since a fix that only checks the failing case could pass by breaking the working one:

- `solution_accessors_report_absent_values` — infeasible solve; all three must report the absence and leave the sentinel intact
- `solution_accessors_return_values_when_present` — solved LP; all three must still return values

Verified locally: **71/71** `C_API_TEST` cases pass.

### Not included

The issue also notes that `cuOptGetProblemStringArrayAttribute` has a related shape problem in the other direction — it requires `count` to match exactly, so "no names set" is indistinguishable from "bad argument", with no size query to ask first. That one needs a small API addition rather than a behaviour fix, so I have left it out of this PR and it can be handled separately.

Authors:
  - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv)

Approvers:
  - Miles Lubin (https://github.com/mlubin)

URL: #1734
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test acd9be0

LPStats and MIPStats are removed, along with Solution.getLPStats and
getMIPStats, their two native declarations and the two JNI entry points that
packed attribute reads into a double[].

Nothing needed to build, solve or read a result goes with them. Solution keeps
the termination status, objectives, MIP gap and solution bound, and variable and
constraint values continue to come from the model. What is dropped is
diagnostics: residuals, iteration and node counts, presolve time, violation
magnitudes, and which method solved the problem.

The capability is not lost, only unexposed. The statistics are scalar solution
attributes in the C API, and every selector already reaches Java through the
generated CuOptConstants, so re-adding them later is a change to this module
alone.

This also removes a fragile contract. LPStats and MIPStats read a positional
double[] whose ordering was defined in the JNI selector lists and consumed by
array index in the constructors, with nothing tying the two together and no test
asserting any statistic's value. Transposing two selectors would have
misreported every field silently.

Docs updated: the API references say where the statistics now live, and the
examples that printed them no longer do. The convex "Inspecting Solutions"
section existed only to demonstrate LPStats and is gone.

Public members across the module go from 195 to 182, and the JNI from 59 native
methods to 57.

Verified: 24/24 Java tests, 57 JNI symbols matched, all pre-commit hooks.
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test d757259

@ramakrishnap-nv ramakrishnap-nv changed the title Java bindings for LP/MIP/QP Java bindings for LP, MIP and QP Aug 18, 2026
Restores access to the statistics, in the shape chris-maes asked for: the
generic getter model already used for problem attributes, applied to solutions.
Solution gains getIntAttribute and getFloatAttribute, keyed by a
CUOPT_SOLUTION_ATTR_* constant, over the C API accessors that merged in NVIDIA#1715.

This is two methods rather than the two classes and eleven getters that were
here before, and a statistic added later is a new constant rather than new Java,
since the selectors are generated into CuOptConstants from constants.h.

It also drops the positional double[] contract the previous LPStats and MIPStats
relied on, where the ordering lived in the JNI selector lists and was consumed
by array index with nothing tying the two together.

Tests assert values rather than that a call returns. An optimal LP has residuals
and a gap within tolerance; a solved MIP has a non-negative node count and a
constraint violation near zero; a float selector through the integer accessor,
and a selector belonging to the other solver, both raise CuOptException.

One thing the value assertions turned up: requesting a method does not mean that
method is credited with the solve. The small LP here reports
CUOPT_METHOD_UNSET even when PDLP is requested, so the test asserts that
solved-by is a method the API defines rather than the one that was asked for.

Verified: 26/26 Java tests, 59 JNI symbols matched, all pre-commit hooks.
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 84a9af6

Two tests, both asserting behaviour rather than that a call returns.

mutatingTheModelChangesTheSolve walks a problem through the Variable setters
and re-solves after each change, checking the answer moves as predicted:
tightening the upper bound moves the optimum, pinning both bounds fixes the
variable, doubling the objective coefficient doubles the objective, and making
the variable integral turns the solve into a MIP so the LP-only accessors start
rejecting. Those setters mutate state that Problem reads at solve time, and only
a changed answer shows the change reached the solver. This matters more now that
relaxing a MIP is documented as setting VariableType rather than calling relax.

mipStartsAndMIPOnlySolutionFields seeds a knapsack through Variable.setMIPStart
and solves. Problem.addMIPStarts collects those by variable index, so a wrong
index would seed the wrong variable; the test pins the optimum and the values of
both variables. It also covers getMIPGap and getSolutionBound, which an optimal
solve determines exactly, and the callback payload, which is a value type and so
can be checked without waiting for the solver to produce an incumbent.

Public API coverage goes from 75% to 86%. What is left is expression arithmetic
and the QuadraticConstraint accessors, which are only reachable through a file
round trip.

Verified: 28/28 Java tests, all pre-commit hooks.
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
# Conflicts:
#	cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 785faec

The java-build job resolves plugins and dependencies from Maven Central with
no warm local repository. A 429 from Central failed the build outright, since
Maven does not retry rate-limited or unavailable responses by default.

Both mvn invocations now carry retry settings for the wagon and native
resolver transports, so the build survives a transient Central failure.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 86cd996c8a51d02a3e70dd21a2cbf65e94b6a1b1

@copy-pr-bot

copy-pr-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

/ok to test 86cd996c8a51d02a3e70dd21a2cbf65e94b6a1b1

@ramakrishnap-nv, there was an error processing your request: E2

See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/2/

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

/ok to test 86cd996

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

Labels

feature request New feature or request non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants