diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f1d4123a6a..6fa33a4391 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -60,6 +60,39 @@ jobs: date: ${{ inputs.date }} sha: ${{ inputs.sha }} script: ci/build_cpp.sh + java-build-matrix: + needs: cpp-build + permissions: + contents: read + uses: rapidsai/shared-workflows/.github/workflows/compute-matrix.yaml@main + with: + build_type: pull-request + matrix_name: conda-cpp-build + matrix_filter: map(select(.ARCH == "amd64")) + java-build: + needs: java-build-matrix + permissions: + actions: read + contents: read + id-token: write + packages: read + pull-requests: read + secrets: inherit # zizmor: ignore[secrets-inherit] + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.java-build-matrix.outputs.matrix) }} + with: + build_type: ${{ inputs.build_type || 'branch' }} + branch: ${{ inputs.branch }} + date: ${{ inputs.date }} + sha: ${{ inputs.sha }} + node_type: "gpu-l4-latest-1" + arch: "amd64" + container_image: "rapidsai/ci-conda:26.08-cuda${{ matrix.CUDA_VER }}-${{ matrix.LINUX_VER }}-py${{ matrix.PY_VER }}" + script: "ci/build_java.sh" + artifact-name: "cuopt-java-cuda${{ matrix.CUDA_VER }}" + file_to_upload: "java/cuopt/target/" python-build: needs: [cpp-build] permissions: @@ -272,6 +305,7 @@ jobs: # so 'test.yaml' can be triggered without waiting for those. needs: - upload-conda + - java-build - wheel-publish-cuopt - wheel-publish-cuopt-server - wheel-publish-cuopt-sh-client diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 8c05b5d1b0..ab71b184cd 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -24,6 +24,8 @@ jobs: - checks - conda-cpp-build - conda-cpp-tests + - java-build-matrix + - java-build - conda-python-build - conda-python-tests - docs-build @@ -165,12 +167,14 @@ jobs: - '!README.md' - '!SECURITY.md' - '!ci/build_wheel*.sh' + - '!ci/build_java.sh' - '!ci/check_style.sh' - '!ci/docker/**' - '!ci/release/**' - '!ci/run_*.pytests.sh' - '!ci/run_ctests.sh' - '!ci/test_cpp*.sh' + - '!ci/test_java.sh' - '!ci/test_notebooks.sh' - '!ci/test_python.sh' - '!ci/test_self_hosted_service.sh' @@ -178,6 +182,7 @@ jobs: - '!ci/thirdparty-testing/**' - '!container-builder/**' - '!helmchart/**' + - '!java/**' - '!ngc/**' - '!sonar-project.properties' - '!sonarqube/**' @@ -223,12 +228,14 @@ jobs: - '!SECURITY.md' - '!agents/**' - '!ci/build_docs.sh' + - '!ci/build_java.sh' - '!ci/build_python.sh' - '!ci/build_wheel*.sh' - '!ci/check_style.sh' - '!ci/docker/**' - '!ci/release/**' - '!ci/test_python.sh' + - '!ci/test_java.sh' - '!ci/test_self_hosted_service.sh' - '!ci/test_wheel*.sh' - '!ci/thirdparty-testing/**' @@ -239,6 +246,7 @@ jobs: - '!gemini-extension.json' - '!helmchart/**' - '!img/**' + - '!java/**' - '!ngc/**' - '!notebooks/**' - '!python/**' @@ -252,6 +260,14 @@ jobs: - '!sonarqube/**' - '!ucf/**' - '!utilities/**' + test_java: + - 'java/**' + - 'ci/build_java.sh' + - 'ci/test_java.sh' + - 'dependencies.yaml' + - '.github/workflows/pr.yaml' + - '.github/workflows/build.yaml' + - '.github/workflows/test.yaml' test_python_conda: - '**' - '!**/*.md' @@ -292,11 +308,13 @@ jobs: - '!SECURITY.md' - '!agents/**' - '!ci/build_docs.sh' + - '!ci/build_java.sh' - '!ci/build_wheel*.sh' - '!ci/check_style.sh' - '!ci/docker/**' - '!ci/release/**' - '!ci/test_self_hosted_service.sh' + - '!ci/test_java.sh' - '!ci/test_wheel*.sh' - '!ci/thirdparty-testing/**' - '!ci/utils/sync_skills_version.sh' @@ -306,6 +324,7 @@ jobs: - '!gemini-extension.json' - '!helmchart/**' - '!img/**' + - '!java/**' - '!ngc/**' - '!notebooks/**' - '!skills/**/SKILL.md' @@ -360,11 +379,13 @@ jobs: - '!ci/build_cpp.sh' - '!ci/build_docs.sh' - '!ci/build_python.sh' + - '!ci/build_java.sh' - '!ci/check_style.sh' - '!ci/docker/**' - '!ci/release/**' - '!ci/run_ctests.sh' - '!ci/test_python.sh' + - '!ci/test_java.sh' - '!ci/thirdparty-testing/**' - '!ci/utils/sync_skills_version.sh' - '!ci/utils/validate_skills.sh' @@ -373,6 +394,7 @@ jobs: - '!gemini-extension.json' - '!helmchart/**' - '!img/**' + - '!java/**' - '!ngc/**' - '!notebooks/**' - '!skills/**/SKILL.md' @@ -393,9 +415,10 @@ jobs: enable_check_generated_files: false conda-cpp-build: needs: [checks, compute-matrix-filters, changed-files] - # Consumed by conda-cpp-tests, conda-python-build, and (transitively) docs-build. + # Consumed by C++, Java, Python, and docs jobs. if: >- fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp || + fromJSON(needs.changed-files.outputs.changed_file_groups).test_java || fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_conda || fromJSON(needs.changed-files.outputs.changed_file_groups).build_docs permissions: @@ -490,6 +513,39 @@ jobs: artifact-name: "cuopt_docs" container_image: "rapidsai/ci-conda:26.08-latest" script: "ci/build_docs.sh" + java-build-matrix: + needs: [conda-cpp-build, changed-files] + permissions: + contents: read + uses: rapidsai/shared-workflows/.github/workflows/compute-matrix.yaml@main + if: >- + fromJSON(needs.changed-files.outputs.changed_file_groups).test_java || + fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp + with: + build_type: pull-request + matrix_name: conda-cpp-build + matrix_filter: map(select(.ARCH == "amd64")) + java-build: + needs: java-build-matrix + permissions: + actions: read + contents: read + id-token: write + packages: read + pull-requests: read + secrets: inherit # zizmor: ignore[secrets-inherit] + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.java-build-matrix.outputs.matrix) }} + with: + build_type: pull-request + node_type: "gpu-l4-latest-1" + arch: "amd64" + container_image: "rapidsai/ci-conda:26.08-cuda${{ matrix.CUDA_VER }}-${{ matrix.LINUX_VER }}-py${{ matrix.PY_VER }}" + script: "ci/test_java.sh" + artifact-name: "cuopt-java-cuda${{ matrix.CUDA_VER }}" + file_to_upload: "java/cuopt/target/" wheel-build-libcuopt: needs: [compute-matrix-filters, changed-files] if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 01c1e2b0b3..059199aa37 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -73,6 +73,31 @@ jobs: script-env-secret-3-key: CUOPT_AWS_SECRET_ACCESS_KEY script-env-secret-3-value: ${{ secrets.CUOPT_AWS_SECRET_ACCESS_KEY }} + conda-java-tests: + permissions: + actions: read + contents: read + id-token: write + packages: read + pull-requests: read + secrets: inherit # zizmor: ignore[secrets-inherit] + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main + strategy: + fail-fast: false + matrix: + cuda_version: + - "12.9.1" + - "13.2.0" + with: + build_type: ${{ inputs.build_type }} + branch: ${{ inputs.branch }} + date: ${{ inputs.date }} + sha: ${{ inputs.sha }} + node_type: "gpu-l4-latest-1" + arch: "amd64" + container_image: "rapidsai/ci-conda:26.08-cuda${{ matrix.cuda_version }}-ubuntu24.04-py3.13" + script: "ci/test_java.sh" + wheel-tests-cuopt: permissions: actions: read @@ -142,6 +167,7 @@ jobs: needs: - conda-cpp-tests - conda-python-tests + - conda-java-tests - wheel-tests-cuopt - wheel-tests-cuopt-server - conda-notebook-tests diff --git a/ci/build_java.sh b/ci/build_java.sh new file mode 100755 index 0000000000..0c754a8fc6 --- /dev/null +++ b/ci/build_java.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +RUN_TESTS=false +if [[ "${1:-}" == "--run-java-tests" ]]; then + RUN_TESTS=true +elif [[ $# -gt 0 ]]; then + echo "Usage: $0 [--run-java-tests]" >&2 + exit 2 +fi + +if [[ -e /opt/conda/etc/profile.d/conda.sh ]]; then + . /opt/conda/etc/profile.d/conda.sh +fi + +rapids-logger "Configuring conda strict channel priority" +conda config --set channel_priority strict + +rapids-logger "Downloading the C++ artifact" +CPP_CHANNEL=$(rapids-download-from-github \ + "$(rapids-artifact-name conda_cpp libcuopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") + +rapids-logger "Generating Java build dependencies" +ENV_YAML_DIR=$(mktemp -d) +rapids-dependency-file-generator \ + --output conda \ + --file-key java \ + --prepend-channel "${CPP_CHANNEL}" \ + --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch)" | tee "${ENV_YAML_DIR}/env.yaml" + +rapids-mamba-retry env create --yes -f "${ENV_YAML_DIR}/env.yaml" -n java \ + --channel "${CPP_CHANNEL}" + +# Temporarily allow unbound variables for conda activation. +set +u +conda activate java +set -u + +rapids-print-env + +export CUOPT_PREFIX="${CONDA_PREFIX}" +export CUOPT_JAVA_NATIVE_BUILD_DIR="${PWD}/java/cuopt/build/native" + +if [[ "${RUN_TESTS}" == true ]]; then + rapids-logger "Building and testing the Java bindings" + bash java/cuopt/scripts/test.sh +else + rapids-logger "Building the Java bindings" + bash java/cuopt/scripts/build_native.sh + mvn -f java/cuopt/pom.xml clean package \ + -DskipTests \ + -Dcuopt.native.dir="${CUOPT_JAVA_NATIVE_BUILD_DIR}" +fi diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index 0e776c6043..af10bd5b9a 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -124,6 +124,11 @@ for DEP in "${DEPENDENCIES[@]}"; do done done +# Update the Java API version. Keep the RAPIDS YY.MM.PP format used by VERSION. +for FILE in java/*/pom.xml; do + sed_runner "/.*/s//${NEXT_FULL_TAG}<\/version>/g" "${FILE}" +done + # Update README.md version badge sed_runner 's/badge\/version-[0-9]\+\.[0-9]\+\.[0-9]\+-blue/badge\/version-'${NEXT_FULL_TAG}'-blue/g' README.md diff --git a/ci/test_java.sh b/ci/test_java.sh new file mode 100755 index 0000000000..40026caa01 --- /dev/null +++ b/ci/test_java.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +rapids-logger "Checking GPU availability" +nvidia-smi + +rapids-logger "Running the Java build and tests" +ci/build_java.sh --run-java-tests diff --git a/dependencies.yaml b/dependencies.yaml index 1251f29f82..73fc663b05 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -45,6 +45,14 @@ files: - test_cpp - test_cpp_cuopt - depends_on_libcuopt + java: + output: none + includes: + - build_common + - cuda + - cuda_version + - depends_on_libcuopt + - java test_python: output: none includes: @@ -275,6 +283,12 @@ dependencies: - libprotobuf - libabseil - re2 + java: + common: + - output_types: conda + packages: + - maven + - openjdk=11.* test_cpp: common: - output_types: [conda] diff --git a/docs/cuopt/source/cuopt-java/convex/convex-api.rst b/docs/cuopt/source/cuopt-java/convex/convex-api.rst new file mode 100644 index 0000000000..28958ed2b8 --- /dev/null +++ b/docs/cuopt/source/cuopt-java/convex/convex-api.rst @@ -0,0 +1,145 @@ +=================================== +Convex Optimization API Reference +=================================== + +The Java LP/MIP/QP bindings are in the package +``com.nvidia.cuopt.mathematicalprogramming``. The public API is documented below by +role. Method names are Java names and therefore use fluent methods instead of +Python operator overloads. + +High-level problem +------------------ + +``Problem`` is the recommended entry point for problems built in Java. + +.. list-table:: ``Problem`` + :header-rows: 1 + :widths: 28 72 + + * - API + - Description + * - ``new Problem()`` / ``new Problem(String name)`` + - Create an empty problem, optionally with a name. + * - ``addVariable(...)`` + - Add a variable with lower/upper bounds, objective coefficient, variable type, and name. + * - ``addConstraint(Constraint, String name)`` + - Add a linear or quadratic constraint. + * - ``setObjective(LinearExpression, ObjectiveSense)`` + - Set a linear objective. + * - ``setObjective(QuadraticExpression, ObjectiveSense)`` + - Set a quadratic objective with optional linear and constant terms. + * - ``solve()`` / ``solve(SolverSettings)`` + - Solve the problem and return a ``Solution``. + * - ``getCSR()`` / ``getQCSR()`` + - Inspect the linear or quadratic objective matrix in CSR form. + * - ``writeMPS(String)`` / ``read(String)`` / ``readMPS(String)`` + - Write or load MPS/QPS-backed problems. The fixed-format overloads accept a boolean flag. + * - ``update()`` / ``updateConstraint(...)`` / ``updateObjective(...)`` + - Update problem state and reset solved values where appropriate. + * - ``relax()`` + - Return a copy with variables converted to continuous type. + +``Problem`` also exposes ``getVariables``, ``getVariable``, ``getConstraints``, +``getConstraint``, ``getNumVariables``, ``getNumConstraints``, +``getNumNonZeros``, ``isMIP``, ``isSolved``, ``getStatus``, +``getObjective``, ``getObjectiveValue``, and ``getSolveTime``. ``getObjective`` +returns the common ``ObjectiveExpression`` type; ``isQuadratic`` distinguishes +the concrete linear and quadratic forms without an ``Object`` downcast. + +``CSRMatrix`` takes ``values``, ``columnIndices``, and ``rowOffsets`` in the +same order used by cuOpt CSR arrays. The arrays are available through +``getValues``, ``getColumnIndices``, and ``getRowOffsets``. + +Variables, expressions, and constraints +---------------------------------------- + +``Variable`` stores the problem index, bounds, objective coefficient, type, +name, solved value, reduced cost, and optional MIP start. Its mutable methods +return the variable so calls can be chained: + +.. code-block:: java + + Variable x = problem.addVariable( + 0.0, Double.POSITIVE_INFINITY, 1.0, + VariableType.CONTINUOUS, "x"); + x.setUpperBound(100.0).setObjectiveCoefficient(2.0); + +``LinearExpression`` supports ``of``, ``ofConstant``, ``plus``, ``minus``, +``times``, ``dividedBy``, ``constant``, and the comparison methods ``le``, +``ge``, and ``eq``. Comparisons return a ``Constraint``. + +``QuadraticExpression`` supports quadratic terms through +``QuadraticExpression.of(first, second, coefficient)`` and the same fluent +arithmetic pattern. It can also contain linear and constant terms. Its +``le`` and ``ge`` methods return quadratic constraints. It does not expose an +``eq`` method because equality quadratic constraints are not supported. + +Both expression classes implement ``ObjectiveExpression``, which exposes the +linear portion, constant, current value, and ``isQuadratic``. + +The enums used in problem construction are: + +* ``ObjectiveSense.MINIMIZE`` and ``ObjectiveSense.MAXIMIZE``; +* ``ConstraintSense.LE``, ``ConstraintSense.GE``, and ``ConstraintSense.EQ``; +* ``VariableType.CONTINUOUS``, ``VariableType.INTEGER``, and + ``VariableType.SEMI_CONTINUOUS``; and +* ``ProblemCategory`` for the native problem classification. + +``ProblemCategory.IP`` is deprecated. Java normalizes the legacy native IP +category to ``ProblemCategory.MIP``. + +``Constraint`` provides ``getSense``, ``getRHS``, ``getCoefficient``, +``getLinearExpression``, ``getQuadraticExpression``, ``isQuadratic``, +``computeSlack``, ``getSlack``, and ``getDualValue``. + +Solver settings +--------------- + +``SolverSettings`` owns native solver configuration and implements +``AutoCloseable``. Settings can be set with the overloaded +``setSetting`` methods for ``String``, ``int``, ``double``, and ``boolean`` +values. Use ``getSetting`` or ``getSettingAsString`` for the native string +representation. The ``getSetting(name, type)`` overload provides a typed +``Boolean``, ``Integer``, ``Double``, or ``String`` result, for example +``getSetting(CuOptConstants.CUOPT_TIME_LIMIT, Double.class)``. + +The settings API also includes: + +* ``getSolverSettingNames`` and the static setting accessors; +* ``setMethod`` and ``setPDLPSolverMode``; +* ``setOptimalityTolerance``; +* ``dumpSettingsToFile`` and ``loadSettingsFromFile``; +* ``toDict``; +* MIP callback registration through ``setMIPCallback``. + +``SolverMethod`` includes ``PDLP``, ``DUAL_SIMPLEX``, ``BARRIER``, +``CONCURRENT``, and ``UNSET``. ``PDLPSolverMode`` exposes the supported PDLP +solver modes. + +Solutions and statistics +------------------------ + +``Solution`` implements ``AutoCloseable`` and exposes: + +* ``getPrimalSolution``, ``getDualSolution``, and ``getReducedCost``; +* ``getPrimalObjective`` and ``getDualObjective``; +* ``getTerminationStatus`` and ``getTerminationReason``; +* ``getErrorStatus`` and ``getErrorMessage``; +* ``getSolveTime`` and ``getProblemCategory``; and +* ``getVars`` when variable names are available. + +LP solutions additionally expose ``getLPStats``. ``LPStats`` contains primal +residual, dual residual, gap, iteration count, and the ``SolverMethod`` used. +MIP-only solution fields are documented in :doc:`../mip/mip-api`. + +MPS and errors +-------------- + +``Problem.read`` and ``Problem.readMPS`` support MPS/QPS parsing, including a +fixed-format boolean overload. ``writeMPS`` writes a problem for round trips +or use by another cuOpt interface. + +Native failures are reported as ``CuOptException`` with a cuOpt status code +available through ``getStatusCode``. Accessing an LP-only field on a MIP +solution, or a MIP-only field on an LP solution, raises +``IllegalStateException``. diff --git a/docs/cuopt/source/cuopt-java/convex/convex-examples.rst b/docs/cuopt/source/cuopt-java/convex/convex-examples.rst new file mode 100644 index 0000000000..2dc775305a --- /dev/null +++ b/docs/cuopt/source/cuopt-java/convex/convex-examples.rst @@ -0,0 +1,135 @@ +============================ +Convex Optimization Examples +============================ + +These examples show the Java modeling patterns corresponding to the Python +LP/QP examples. They assume the Java module has been compiled as described in +:doc:`../quick-start` and that the application can load ``libcuopt_jni``. + +Simple linear programming +-------------------------- + +The high-level API uses fluent expressions and explicit comparison methods. + +.. code-block:: java + + import com.nvidia.cuopt.mathematicalprogramming.*; + + try (Problem problem = new Problem("simple-lp")) { + Variable x = problem.addVariable( + 0.0, Double.POSITIVE_INFINITY, 1.0, + VariableType.CONTINUOUS, "x"); + Variable y = problem.addVariable( + 0.0, Double.POSITIVE_INFINITY, 1.0, + VariableType.CONTINUOUS, "y"); + + problem.addConstraint( + LinearExpression.of(x).plus(y).ge(10.0), "demand"); + problem.setObjective( + LinearExpression.of(x).plus(y), ObjectiveSense.MINIMIZE); + + try (SolverSettings settings = new SolverSettings() + .setMethod(SolverMethod.PDLP); + Solution solution = problem.solve(settings)) { + System.out.println("Status: " + solution.getTerminationStatus()); + System.out.println("x = " + x.getValue()); + System.out.println("y = " + y.getValue()); + System.out.println("Objective = " + solution.getPrimalObjective()); + } + } + +``Problem.solve`` populates the ``Variable`` and ``Constraint`` objects after +the solve. The solution object remains available for detailed native results +and statistics. + +Simple quadratic programming +----------------------------- + +Quadratic objectives combine quadratic, linear, and constant terms: + +.. code-block:: java + + try (Problem problem = new Problem("simple-qp")) { + Variable x = problem.addVariable(0.0, 10.0, 0.0, VariableType.CONTINUOUS, "x"); + Variable y = problem.addVariable(0.0, 10.0, 0.0, VariableType.CONTINUOUS, "y"); + + QuadraticExpression objective = QuadraticExpression + .of(x, x, 1.0) + .plus(y, y, 1.0) + .plus(LinearExpression.of(x).times(-1.0)) + .plus(LinearExpression.of(y).times(-1.0)); + + problem.addConstraint( + LinearExpression.of(x).plus(y).eq(1.0), "sum"); + problem.setObjective(objective, ObjectiveSense.MINIMIZE); + + try (Solution solution = problem.solve()) { + System.out.println("x = " + x.getValue()); + System.out.println("y = " + y.getValue()); + System.out.println("Objective = " + solution.getPrimalObjective()); + System.out.println("LP stats gap = " + solution.getLPStats().getGap()); + } + } + +For QP solutions, ``getPrimalSolution``, ``getDualSolution``, +``getReducedCost``, ``getDualObjective``, and ``getLPStats`` are available when +the solver returns the corresponding values. + +Quadratic constraints +--------------------- + +Quadratic constraints can be added directly to a ``Problem``: + +.. code-block:: java + + try (Problem problem = new Problem("quadratic-constraint")) { + Variable x = problem.addVariable(0.0, 10.0, 1.0, VariableType.CONTINUOUS, "x"); + Variable y = problem.addVariable(0.0, 10.0, 1.0, VariableType.CONTINUOUS, "y"); + + QuadraticExpression radius = QuadraticExpression + .of(x, x, 1.0) + .plus(y, y, 1.0); + problem.addConstraint(radius.le(4.0), "radius"); + problem.setObjective( + LinearExpression.of(x).plus(y), ObjectiveSense.MAXIMIZE); + + try (Solution solution = problem.solve()) { + System.out.println(solution.getTerminationStatus()); + } + } + +Only ``LE`` and ``GE`` quadratic constraints are supported; +``QuadraticExpression`` does not expose an ``eq`` method. + +Reading and writing MPS/QPS +--------------------------- + +``Problem`` exposes both extension-dispatch and direct MPS entry points: + +.. code-block:: java + + try (Problem problem = Problem.read("problem.mps")) { + System.out.println("Variables: " + problem.getNumVariables()); + problem.writeMPS("roundtrip.mps"); + } + + try (Problem fixed = Problem.readMPS("fixed-format.mps", true)) { + // Use fixed-format parsing explicitly. + } + +Parsing failures are reported as ``CuOptException`` with the cuOpt status code +available from ``getStatusCode``. + +Inspecting solutions +-------------------- + +LP solutions expose residuals and solver metadata through ``LPStats``: + +.. code-block:: java + + try (SolverSettings settings = new SolverSettings().setMethod(SolverMethod.PDLP); + Solution solution = problem.solve(settings)) { + LPStats stats = solution.getLPStats(); + System.out.println(stats.getNumIterations()); + System.out.println(stats.getPrimalResidual()); + } diff --git a/docs/cuopt/source/cuopt-java/convex/index.rst b/docs/cuopt/source/cuopt-java/convex/index.rst new file mode 100644 index 0000000000..498fafcd85 --- /dev/null +++ b/docs/cuopt/source/cuopt-java/convex/index.rst @@ -0,0 +1,18 @@ +================================================ +Convex Optimization (LP/QP/QCQP/SOCP) +================================================ + +This section documents the Java bindings for LP, QP, QCQP, and SOCP. The Java +API is based on ``Problem``, ``Variable``, expressions, and constraints. + +Quadratic constraints are supported for ``LE`` and ``GE`` constraints. Equality +quadratic constraints are rejected by the Java API. + +.. toctree:: + :maxdepth: 3 + :caption: LP/QP/QCQP/SOCP Java API + :name: LP/QP/QCQP/SOCP Java API Reference + :titlesonly: + + convex-api.rst + convex-examples.rst diff --git a/docs/cuopt/source/cuopt-java/index.rst b/docs/cuopt/source/cuopt-java/index.rst new file mode 100644 index 0000000000..d7b30c63a3 --- /dev/null +++ b/docs/cuopt/source/cuopt-java/index.rst @@ -0,0 +1,44 @@ +==================================== +Java API +==================================== + +NVIDIA cuOpt provides experimental Java bindings for linear programming (LP), +mixed-integer linear programming (MILP), quadratic programming (QP), +quadratically constrained quadratic programming (QCQP), and second-order cone +programming (SOCP) through JNI. + +The Java bindings are a separately compiled beta module for this mathematical +programming surface. Repository CI and release workflows build and test it +against the matching ``libcuopt`` artifact, but it is not part of the top-level +cuOpt build and does not provide routing or distance-engine bindings. See +:doc:`quick-start` before using the API. + +.. note:: + + Build the module locally from ``java/cuopt`` against an existing cuOpt + installation. CI artifacts are experimental; publication to a supported + Maven repository has not been defined. + +.. toctree:: + :maxdepth: 3 + :caption: Java API Overview + :name: Java API Overview + :titlesonly: + + quick-start.rst + +.. toctree:: + :maxdepth: 3 + :caption: Convex Optimization (LP/QP/QCQP/SOCP) + :name: LP/QP Java API + :titlesonly: + + Convex Optimization + +.. toctree:: + :maxdepth: 3 + :caption: Mixed Integer Programming (MIP) + :name: MIP Java API + :titlesonly: + + Mixed Integer Programming diff --git a/docs/cuopt/source/cuopt-java/mip/index.rst b/docs/cuopt/source/cuopt-java/mip/index.rst new file mode 100644 index 0000000000..afdc154b97 --- /dev/null +++ b/docs/cuopt/source/cuopt-java/mip/index.rst @@ -0,0 +1,24 @@ +===================================== +Mixed Integer Programming (MIP) +===================================== + +.. note:: + + The cuOpt MIP solver is in beta and under active development. Finding + high-quality feasible solutions and proving optimality have different + performance characteristics; inspect termination status, MIP gap, and + solution bound in production workflows. + +The Java MIP API uses the same ``Problem``, ``SolverSettings``, and +``Solution`` classes as the LP/QP API. MIP problems are +identified by one or more variables with ``INTEGER`` or ``SEMI_CONTINUOUS`` +type. + +.. toctree:: + :maxdepth: 3 + :caption: MIP Java API + :name: MIP Java API Reference + :titlesonly: + + mip-api.rst + mip-examples.rst diff --git a/docs/cuopt/source/cuopt-java/mip/mip-api.rst b/docs/cuopt/source/cuopt-java/mip/mip-api.rst new file mode 100644 index 0000000000..ebdfcb2d4b --- /dev/null +++ b/docs/cuopt/source/cuopt-java/mip/mip-api.rst @@ -0,0 +1,118 @@ +================= +MIP API Reference +================= + +MIP uses the shared Java problem construction and solve APIs documented in +:doc:`../convex/convex-api`. The following features are particularly relevant +to mixed-integer problems. + +Variable types +-------------- + +Use ``VariableType`` when adding a variable or updating an existing variable: + +.. code-block:: java + + Variable integer = problem.addVariable( + 0.0, 100.0, 3.0, + VariableType.INTEGER, "integer"); + + Variable semiContinuous = problem.addVariable( + 0.0, 100.0, 1.0, + VariableType.SEMI_CONTINUOUS, "semi"); + +The supported values are ``CONTINUOUS``, ``INTEGER``, and +``SEMI_CONTINUOUS``. ``Problem.isMIP()`` and ``Solution.isMIP()`` report +whether a problem or result contains a noncontinuous variable. + +MIP starts +---------- + +MIP starts can be provided per variable through ``Variable.setMIPStart``. The +high-level ``Problem.solve`` collects defined variable starts and passes them +to the native solver. A complete start can also be supplied directly through +``SolverSettings.addMIPStart(double[])``. + +.. code-block:: java + + x.setMIPStart(3.0); + y.setMIPStart(2.0); + + try (SolverSettings settings = new SolverSettings()) { + // The array follows the problem's variable-index order. + settings.addMIPStart(new double[] {3.0, 2.0}); + try (Solution solution = problem.solve(settings)) { + System.out.println(solution.getMIPGap()); + } + } + +MIP settings +------------ + +All solver settings are set through ``SolverSettings``. Use the overloaded +``setSetting`` methods for string, integer, floating-point, and boolean +values. MIP-relevant settings include time and node limits, MIP tolerances, +presolve, heuristics, scaling, determinism, and cut controls. Setting names +are available at runtime through ``SolverSettings.getSolverSettingNames``; +the generated ``CuOptConstants`` class contains string and integer constants +from the cuOpt public constants header. + +MIP callbacks +------------- + +``SolverSettings.setMIPCallback`` accepts either callback interface: + +``MIPSolutionCallback`` receives each incumbent solution: + +.. code-block:: java + + settings.setMIPCallback( + (solution, objectiveValue, solutionBound, userData) -> { + System.out.println("incumbent objective = " + objectiveValue); + }, + "my-user-data", + problem.getNumVariables()); + +``MIPSetSolutionCallback`` returns a candidate solution and objective when the +native solver asks Java for one: + +.. code-block:: java + + settings.setMIPCallback( + (solutionBound, userData) -> + new MIPCallbackSolution(new double[] {3.0, 2.0}, 19.0), + null, + problem.getNumVariables()); + +Callbacks are native-runtime features. Keep the callback and any user data +valid for the duration of the solve, and close the ``SolverSettings`` after the +solve completes. Registered callbacks can be inspected with +``getMIPCallbacks``. + +MIP solution fields +------------------- + +For a MIP ``Solution``: + +* ``getPrimalSolution`` returns the incumbent primal vector; +* ``getPrimalObjective`` returns its objective value; +* ``getMIPGap`` returns the current relative MIP gap; +* ``getSolutionBound`` returns the best bound reported by the solver; +* ``getMIPStats`` returns ``MIPStats``; and +* ``getTerminationStatus``, ``getTerminationReason``, ``getErrorStatus``, + ``getErrorMessage``, and ``getSolveTime`` describe the solve. + +``MIPStats`` contains presolve time, maximum constraint violation, maximum +integer violation, maximum variable-bound violation, node count, and simplex +iteration count. LP-only accessors such as ``getDualSolution``, +``getReducedCost``, and ``getLPStats`` raise ``IllegalStateException`` for a +MIP result. + +Relaxing and inspecting a MIP +----------------------------- + +``Problem.relax()`` returns a separate continuous problem while preserving +variable names, bounds, objective, and constraints. This is useful for +inspecting the LP relaxation without changing the original MIP. The original +problem can also be inspected through ``getCSR``, ``getQCSR``, and +``getIncumbentValues``. diff --git a/docs/cuopt/source/cuopt-java/mip/mip-examples.rst b/docs/cuopt/source/cuopt-java/mip/mip-examples.rst new file mode 100644 index 0000000000..b482a5e728 --- /dev/null +++ b/docs/cuopt/source/cuopt-java/mip/mip-examples.rst @@ -0,0 +1,115 @@ +============ +MIP Examples +============ + +These examples show mixed-integer modeling, MIP starts, semi-continuous +variables, and incumbent callbacks in Java. + +Simple MILP +----------- + +.. code-block:: java + + import com.nvidia.cuopt.mathematicalprogramming.*; + + try (Problem problem = new Problem("simple-milp")) { + Variable x = problem.addVariable( + 0.0, 100.0, 3.0, VariableType.INTEGER, "x"); + Variable y = problem.addVariable( + 0.0, 100.0, 5.0, VariableType.INTEGER, "y"); + + problem.addConstraint( + LinearExpression.of(x).times(2.0).plus(y).le(8.0), "capacity"); + problem.setObjective( + LinearExpression.of(x).times(3.0).plus(y, 5.0), + ObjectiveSense.MAXIMIZE); + + try (SolverSettings settings = new SolverSettings() + .setSetting(CuOptConstants.CUOPT_TIME_LIMIT, 10.0); + Solution solution = problem.solve(settings)) { + System.out.println("Status: " + solution.getTerminationStatus()); + System.out.println("x = " + x.getValue()); + System.out.println("y = " + y.getValue()); + System.out.println("Objective = " + solution.getPrimalObjective()); + System.out.println("MIP gap = " + solution.getMIPGap()); + System.out.println("Bound = " + solution.getSolutionBound()); + System.out.println("Nodes = " + solution.getMIPStats().getNumNodes()); + } + } + +The MIP solver can return a feasible solution before proving optimality. Use +the termination status, MIP gap, and solution bound together when interpreting +the result. + +Semi-continuous variables +------------------------- + +``SEMI_CONTINUOUS`` variables are zero or lie within their declared bounds. + +.. code-block:: java + + try (Problem problem = new Problem("semi-continuous")) { + Variable production = problem.addVariable( + 10.0, 100.0, 1.0, + VariableType.SEMI_CONTINUOUS, "production"); + problem.setObjective(production, ObjectiveSense.MINIMIZE); + + try (Solution solution = problem.solve()) { + System.out.println("production = " + production.getValue()); + } + } + +MIP starts +---------- + +Set starts on variables when using the high-level ``Problem`` API: + +.. code-block:: java + + x.setMIPStart(3.0); + y.setMIPStart(2.0); + + try (SolverSettings settings = new SolverSettings(); + Solution solution = problem.solve(settings)) { + System.out.println(solution.getPrimalObjective()); + } + +For the deprecated lower-level representation, pass a full +variable-index-ordered array through +``SolverSettings.addMIPStart``. + +Incumbent callback +------------------ + +Register an incumbent callback before solving: + +.. code-block:: java + + try (SolverSettings settings = new SolverSettings()) { + settings.setMIPCallback( + (incumbent, objective, bound, userData) -> { + System.out.println( + "incumbent objective=" + objective + ", bound=" + bound); + }, + null, + problem.getNumVariables()); + + try (Solution solution = problem.solve(settings)) { + System.out.println("Final status: " + solution.getTerminationStatus()); + } + } + +The callback receives a defensive Java array containing the incumbent vector, +the incumbent objective, the current solution bound, and the user data object. + +LP relaxation +------------- + +Create a continuous relaxation without changing the original MIP: + +.. code-block:: java + + try (Problem relaxed = problem.relax(); + Solution solution = relaxed.solve()) { + System.out.println("LP relaxation objective = " + solution.getPrimalObjective()); + } diff --git a/docs/cuopt/source/cuopt-java/quick-start.rst b/docs/cuopt/source/cuopt-java/quick-start.rst new file mode 100644 index 0000000000..a1853e2939 --- /dev/null +++ b/docs/cuopt/source/cuopt-java/quick-start.rst @@ -0,0 +1,159 @@ +Java Quick Start +================ + +The experimental Java bindings live in ``java/cuopt`` and are built explicitly +from source. Repository CI and release workflows also build and test the module +against the matching ``libcuopt`` artifact. It is not part of the top-level +cuOpt build, and a supported Maven distribution has not yet been defined. + +Requirements +------------ + +The Java module requires: + +* Java 11 or newer, with ``JAVA_HOME`` pointing to a JDK; +* a C++20 compiler; +* an existing cuOpt installation containing ``libcuopt.so``; and +* a CUDA-enabled runtime for solving problems. + +The module uses Maven for Java compilation and a Java-local CMake project for +the JNI library. The standalone native build links to +``$CUOPT_PREFIX/lib/libcuopt.so`` and places ``libcuopt_jni.so`` under +``java/cuopt/build/native``. + +.. code-block:: bash + + cd /path/to/cuopt/java/cuopt + export JAVA_HOME=/path/to/jdk-11 + export CUOPT_PREFIX=/path/to/cuopt/conda/environment + bash scripts/build_native.sh + +This builds ``java/cuopt/build/native/libcuopt_jni.so``. Java is intentionally +not part of the default cuOpt build. + +To build the native library in a different directory, set +``CUOPT_JAVA_NATIVE_BUILD_DIR``. If CUDA headers are installed outside the +usual locations, pass ``-DCUOPT_CUDA_INCLUDE_DIR=/path/to/cuda/include`` to +the CMake configure step. + +Native Loading +-------------- + +At runtime the bindings load ``libcuopt_jni``. For local development, point Java +at the directory containing the built native library: + +.. code-block:: bash + + cd java/cuopt + export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 + export CUOPT_PREFIX=/path/to/cuopt/conda/environment + export LD_LIBRARY_PATH=$CUOPT_PREFIX/targets/x86_64-linux/lib:$CUOPT_PREFIX/lib:build/native + mvn test -Dcuopt.native.dir=build/native + +The helper script combines the native build and Maven test steps: + +.. code-block:: bash + + cd /path/to/cuopt/java/cuopt + export JAVA_HOME=/path/to/jdk-11 + export CUOPT_PREFIX=/path/to/cuopt/conda/environment + bash scripts/test.sh + +To run one test class, pass its Maven property to the helper: + +.. code-block:: bash + + bash scripts/test.sh -Dtest=ProblemIntegrationTest + +Application code can use the same property: + +.. code-block:: bash + + java -Dcuopt.native.dir=/path/to/java/cuopt/build/native ... + +The Java classes load ``libcuopt_jni`` when the first binding object is +created. ``cuopt.native.dir`` must contain that library, and the cuOpt and +CUDA runtime libraries must be discoverable through ``LD_LIBRARY_PATH`` or the +native library's runtime path. The standalone native build embeds the CUDA +runtime path for the configured ``CUOPT_PREFIX``; the helper script also +exports it for Maven. + +LP Example +---------- + +The modeling API mirrors the Python concepts while using Java builder methods +instead of operator overloading. + +.. code-block:: java + + import com.nvidia.cuopt.mathematicalprogramming.*; + + Problem problem = new Problem("simple"); + Variable x = problem.addVariable(0, Double.POSITIVE_INFINITY, 0, + VariableType.CONTINUOUS, "x"); + Variable y = problem.addVariable(0, Double.POSITIVE_INFINITY, 0, + VariableType.CONTINUOUS, "y"); + + problem.addConstraint(LinearExpression.of(x).plus(y).ge(1.0), "c0"); + problem.setObjective(LinearExpression.of(x).plus(y), ObjectiveSense.MINIMIZE); + + try (SolverSettings settings = new SolverSettings().setMethod(SolverMethod.PDLP); + Solution solution = problem.solve(settings)) { + System.out.println(solution.getTerminationStatus()); + System.out.println(solution.getPrimalObjective()); + System.out.println(solution.getLPStats().getSolvedBy()); + } + +MILP Example +------------ + +.. code-block:: java + + Problem problem = new Problem("integer"); + Variable x = problem.addVariable(0, 10, 1.0, VariableType.INTEGER, "x"); + problem.addConstraint(LinearExpression.of(x).ge(1.0)); + + try (SolverSettings settings = new SolverSettings() + .setSetting(CuOptConstants.CUOPT_TIME_LIMIT, 10.0); + Solution solution = problem.solve(settings)) { + System.out.println(solution.getMIPGap()); + System.out.println(solution.getMIPStats().getNumNodes()); + } + +QP Example +---------- + +.. code-block:: java + + try (Problem problem = new Problem("quadratic")) { + Variable x = problem.addVariable(0.0, 10.0, 0.0, VariableType.CONTINUOUS, "x"); + Variable y = problem.addVariable(0.0, 10.0, 0.0, VariableType.CONTINUOUS, "y"); + problem.addConstraint(LinearExpression.of(x).plus(y).ge(5.0)); + problem.setObjective( + QuadraticExpression.of(x, x, 1.0).plus(y, y, 4.0), + ObjectiveSense.MINIMIZE); + try (Solution solution = problem.solve()) { + System.out.println(solution.getPrimalObjective()); + } + } + +MPS I/O +------- + +.. code-block:: java + + try (Problem problem = Problem.read("problem.mps")) { + problem.writeMPS("roundtrip.mps"); + } + +Lifecycle +--------- + +``SolverSettings`` and ``Solution`` own native handles and implement +``AutoCloseable``. Prefer try-with-resources. They also register a ``Cleaner`` +fallback, but deterministic close keeps native memory pressure predictable. + +The Java module is not a drop-in translation of Python syntax. Java uses +fluent expression methods such as ``plus``, ``minus``, ``le``, ``ge``, and +``eq`` instead of Python operator overloads. The following pages document the +implemented LP/MILP/QP/QCQP/SOCP surface and its Java names. diff --git a/docs/cuopt/source/index.rst b/docs/cuopt/source/index.rst index 3d53b88965..4bc291112e 100644 --- a/docs/cuopt/source/index.rst +++ b/docs/cuopt/source/index.rst @@ -42,6 +42,16 @@ Python (cuopt) Python Overview +========================== +Java (cuopt) +========================== +.. toctree:: + :maxdepth: 4 + :caption: Java API + :name: Java API + + Java Overview + ==================================== gRPC remote execution ==================================== diff --git a/java/.gitignore b/java/.gitignore new file mode 100644 index 0000000000..3d9173556a --- /dev/null +++ b/java/.gitignore @@ -0,0 +1,3 @@ +*.iml +hs_err*.log +target/ diff --git a/java/cuopt/CMakeLists.txt b/java/cuopt/CMakeLists.txt new file mode 100644 index 0000000000..9cee48904f --- /dev/null +++ b/java/cuopt/CMakeLists.txt @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.22) +project(cuopt_java_native LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +find_package(JNI REQUIRED) + +set(CUOPT_PREFIX "" CACHE PATH "cuOpt installation prefix") +if(NOT CUOPT_PREFIX) + set(CUOPT_PREFIX "$ENV{CONDA_PREFIX}") +endif() +if(NOT CUOPT_PREFIX) + message(FATAL_ERROR "Set CUOPT_PREFIX to the cuOpt conda environment") +endif() + +set(CUOPT_INCLUDE_DIR "${CUOPT_PREFIX}/include") +set(CUOPT_LIBRARY "${CUOPT_PREFIX}/lib/libcuopt.so") +set(CUOPT_CUDA_RUNTIME_DIR "${CUOPT_PREFIX}/targets/x86_64-linux/lib" + CACHE PATH "CUDA runtime library directory") +set(CUOPT_CUDA_INCLUDE_DIR "" CACHE PATH "CUDA toolkit include directory") +if(NOT CUOPT_CUDA_INCLUDE_DIR AND DEFINED ENV{CUDA_HOME}) + set(CUOPT_CUDA_INCLUDE_DIR "$ENV{CUDA_HOME}/include") +endif() +if(NOT CUOPT_CUDA_INCLUDE_DIR AND EXISTS "/usr/local/cuda/include") + set(CUOPT_CUDA_INCLUDE_DIR "/usr/local/cuda/include") +endif() +if(NOT EXISTS "${CUOPT_LIBRARY}") + message(FATAL_ERROR "cuOpt shared library was not found at ${CUOPT_LIBRARY}") +endif() +set(CUOPT_CUDART_LIBRARY "${CUOPT_CUDA_RUNTIME_DIR}/libcudart.so") +if(NOT EXISTS "${CUOPT_CUDART_LIBRARY}") + message(FATAL_ERROR "CUDA runtime library was not found at ${CUOPT_CUDART_LIBRARY}") +endif() + +add_library(cuopt_jni SHARED + src/main/native/cuopt_java_native_api.cpp + src/main/native/cuopt_jni.cpp) + +target_include_directories(cuopt_jni PRIVATE + ${JNI_INCLUDE_DIRS} + ${CUOPT_INCLUDE_DIR} + ${CUOPT_PREFIX}/include/rapids + ${CUOPT_PREFIX}/targets/x86_64-linux/include + ${CUOPT_CUDA_INCLUDE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../cpp/src) + +target_link_libraries(cuopt_jni PRIVATE "${CUOPT_LIBRARY}" "${CUOPT_CUDART_LIBRARY}") + +# The Java module is built outside the main cuOpt build. Keep its native +# loader self-contained while allowing the script to add the cuOpt runtime +# directory supplied by the selected conda environment. +set_target_properties(cuopt_jni PROPERTIES + BUILD_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN") + +if(CUOPT_RUNTIME_LIBRARY_DIR) + set_property(TARGET cuopt_jni APPEND PROPERTY + BUILD_RPATH "${CUOPT_RUNTIME_LIBRARY_DIR}") +endif() + +set_property(TARGET cuopt_jni APPEND PROPERTY BUILD_RPATH "${CUOPT_CUDA_RUNTIME_DIR}") +set_property(TARGET cuopt_jni APPEND PROPERTY INSTALL_RPATH "${CUOPT_CUDA_RUNTIME_DIR}") diff --git a/java/cuopt/README.md b/java/cuopt/README.md new file mode 100644 index 0000000000..e17848704f --- /dev/null +++ b/java/cuopt/README.md @@ -0,0 +1,42 @@ +# cuOpt Java bindings + +This directory contains a source module for cuOpt LP, MILP, QP, +QCQP, and SOCP Java bindings. The repository CI and release workflows build +and test the module against the matching `libcuopt` conda artifact and retain +the Maven `target/` output as a workflow artifact. Publication to a supported +Maven repository has not been defined. + +The module is not connected to the repository-level `build.sh` or main CMake +targets. + +For local development, compile the Java module against an existing cuOpt +installation: + +```bash +cd java/cuopt +CUOPT_PREFIX=/path/to/cuopt/conda/environment bash scripts/build_native.sh +CUOPT_PREFIX=/path/to/cuopt/conda/environment bash scripts/test.sh +``` + +`build_native.sh` builds `libcuopt_jni.so` in `build/native`. `test.sh` builds +that library and runs the Maven tests. Java 11 or newer and a C++20 compiler +are required. Native solve tests require a CUDA driver and skip automatically +when one is unavailable. + +The standalone native project links to `${CUOPT_PREFIX}/lib/libcuopt.so` and +uses private cuOpt headers from the checkout to implement the Java-local +bridge. The Java-required native extensions need C API review before this +module can become a supported binary distribution. No Java-specific symbol or +source file is currently required by the main cuOpt build. + +## Generated constants + +Maven generates `CuOptConstants.java` under +`target/generated-sources/cuopt/com/nvidia/cuopt/mathematicalprogramming/` +from `cpp/include/cuopt/linear_programming/constants.h`. Do not edit the +generated file. Regenerate it after changing the C++ constants header with: + +```bash +cd java/cuopt +mvn generate-sources +``` diff --git a/java/cuopt/TESTS.md b/java/cuopt/TESTS.md new file mode 100644 index 0000000000..526b530c0c --- /dev/null +++ b/java/cuopt/TESTS.md @@ -0,0 +1,73 @@ + + +# cuOpt Java binding tests + +The Java module has three test classes under +`src/test/java/com/nvidia/cuopt/mathematicalprogramming`: + +- `ProblemModelingTest`: five pure Java modeling tests. +- `NativeIntegrationTest`: seven JNI/native cuOpt smoke tests. +- `ProblemIntegrationTest`: ten standalone Java problem and solve tests. + +The suite has no dependency on the cuOpt Python interface. + +## How to run + +Build the JNI library and run all Java tests with: + +```bash +cd /path/to/cuopt/java/cuopt +export JAVA_HOME=/path/to/jdk-11 +export CUOPT_PREFIX=/path/to/cuopt/conda/environment +bash scripts/test.sh +``` + +To run one test class: + +```bash +bash scripts/test.sh -Dtest=ProblemIntegrationTest +``` + +When invoking Maven directly, build the JNI library first and provide the +native directory: + +```bash +bash scripts/build_native.sh +mvn test -Dcuopt.native.dir=build/native +``` + +`scripts/test.sh` configures `LD_LIBRARY_PATH` for the selected cuOpt and CUDA +runtime libraries. + +## Coverage + +`ProblemModelingTest` exercises generated enum constants, legacy problem +category mapping, expression construction, CSR generation, duplicate-term +merging, problem updates, relaxation, and quadratic inspection without loading +the native library. + +`NativeIntegrationTest` covers settings, setting-file round trips, LP/MILP/QP +solves, solution statistics, error propagation, and MPS read/write paths. + +`ProblemIntegrationTest` constructs ten LP, MILP, and QP cases entirely in +Java. Each dynamic test verifies the public `Problem` model built from the +case data. The LP/MILP cases also check solve status, variable bounds, +integrality, constraint feasibility, objective values, and type-specific +solution behavior. The QP case verifies quadratic-objective and +quadratic-constraint construction; QP solve callability is covered by +`NativeIntegrationTest`. The cases cover minimization and maximization, +equality and ranged constraints, mixed bounds, mixed integer/continuous +variables, metadata, and infeasibility. + +## Prerequisite behavior + +- Pure modeling tests run without JNI or a GPU. +- Native tests skip when `cuopt.native.dir` is unset or `libcuopt_jni` is not + present. +- Solve tests skip when a CUDA driver is unavailable. + +The expected successful Maven result is `BUILD SUCCESS`. If a forked JVM +crashes, inspect `target/surefire-reports`. diff --git a/java/cuopt/pom.xml b/java/cuopt/pom.xml new file mode 100644 index 0000000000..6584f703c7 --- /dev/null +++ b/java/cuopt/pom.xml @@ -0,0 +1,101 @@ + + + + 4.0.0 + + com.nvidia.cuopt + cuopt + + 26.08.00 + cuOpt Java Bindings + Java JNI bindings for cuOpt numerical optimization. + + + 11 + UTF-8 + 5.11.4 + + + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${maven.compiler.release} + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + + generate-cuopt-constants + generate-sources + + exec + + + ${project.basedir}/scripts/generate_constants.sh + + ${project.basedir}/../../cpp/include/cuopt/linear_programming/constants.h + ${project.build.directory}/generated-sources/cuopt + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + add-generated-sources + generate-sources + + add-source + + + + ${project.build.directory}/generated-sources/cuopt + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + false + + ${cuopt.native.dir} + + + + + + diff --git a/java/cuopt/scripts/build_native.sh b/java/cuopt/scripts/build_native.sh new file mode 100644 index 0000000000..e9bcdc0216 --- /dev/null +++ b/java/cuopt/scripts/build_native.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +MODULE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_ROOT="$(cd "${MODULE_DIR}/../.." && pwd)" +CUOPT_PREFIX="${CUOPT_PREFIX:-${CONDA_PREFIX:-${REPO_ROOT}/.cuopt_env}}" +BUILD_DIR="${CUOPT_JAVA_NATIVE_BUILD_DIR:-${MODULE_DIR}/build/native}" + +if [[ -z "${JAVA_HOME:-}" ]]; then + JAVAC_PATH="$(command -v javac || true)" + if [[ -n "${JAVAC_PATH}" ]]; then + JAVA_HOME="$(dirname "$(dirname "$(readlink -f "${JAVAC_PATH}")")")" + export JAVA_HOME + fi +fi + +if [[ ! -x "${JAVA_HOME:-}/bin/javac" ]]; then + echo "JAVA_HOME must point to a JDK containing bin/javac (Java 11 is required)." >&2 + exit 1 +fi + +CMAKE="${CMAKE:-}" +if [[ -z "${CMAKE}" && -x "${CUOPT_PREFIX}/bin/cmake" ]]; then + CMAKE="${CUOPT_PREFIX}/bin/cmake" +fi +if [[ -z "${CMAKE}" ]]; then + CMAKE="$(command -v cmake || true)" +fi +if [[ -z "${CMAKE}" ]]; then + echo "cmake was not found; use the cuOpt conda environment or set CMAKE." >&2 + exit 1 +fi + +if [[ ! -f "${CUOPT_PREFIX}/lib/libcuopt.so" ]]; then + echo "cuOpt shared library was not found at ${CUOPT_PREFIX}/lib/libcuopt.so." >&2 + exit 1 +fi + +CXX_COMPILER="${CXX:-}" +if [[ -z "${CXX_COMPILER}" && -f "${BUILD_DIR}/CMakeCache.txt" ]]; then + CACHED_CXX_COMPILER="$(sed -n 's/^CMAKE_CXX_COMPILER:.*=//p' "${BUILD_DIR}/CMakeCache.txt" | head -n 1)" + if [[ -x "${CACHED_CXX_COMPILER}" ]]; then + CXX_COMPILER="${CACHED_CXX_COMPILER}" + fi +fi +if [[ -z "${CXX_COMPILER}" && -x "${CUOPT_PREFIX}/bin/c++" ]]; then + CXX_COMPILER="${CUOPT_PREFIX}/bin/c++" +fi + +env -u CFLAGS -u CXXFLAGS -u CPPFLAGS -u LDFLAGS \ + "${CMAKE}" -S "${MODULE_DIR}" -B "${BUILD_DIR}" \ + -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE:-Release}" \ + -DCUOPT_PREFIX="${CUOPT_PREFIX}" \ + -DCUOPT_RUNTIME_LIBRARY_DIR="${CUOPT_PREFIX}/lib" \ + ${CXX_COMPILER:+-DCMAKE_CXX_COMPILER="${CXX_COMPILER}"} \ + -DJAVA_HOME="${JAVA_HOME}" + +env -u CFLAGS -u CXXFLAGS -u CPPFLAGS -u LDFLAGS \ + "${CMAKE}" --build "${BUILD_DIR}" --target cuopt_jni \ + --parallel "${PARALLEL_LEVEL:-2}" + +echo "Built ${BUILD_DIR}/libcuopt_jni.so" diff --git a/java/cuopt/scripts/generate_constants.sh b/java/cuopt/scripts/generate_constants.sh new file mode 100755 index 0000000000..faf5a2d1b9 --- /dev/null +++ b/java/cuopt/scripts/generate_constants.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +HEADER=${1:?missing constants.h path} +OUT_DIR=${2:?missing output directory} +PACKAGE_DIR="${OUT_DIR}/com/nvidia/cuopt/mathematicalprogramming" +OUT_FILE="${PACKAGE_DIR}/CuOptConstants.java" + +mkdir -p "${PACKAGE_DIR}" + +{ + echo "/*" + echo " * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved." + echo " * SPDX-License-Identifier: Apache-2.0" + echo " *" + echo " * AUTO-GENERATED FILE. DO NOT EDIT." + echo " * Generated from cpp/include/cuopt/linear_programming/constants.h." + echo " * Run 'mvn generate-sources' from java/cuopt to regenerate." + echo " */" + echo "package com.nvidia.cuopt.mathematicalprogramming;" + echo + echo "public final class CuOptConstants {" + echo " private CuOptConstants() {}" + echo + awk ' + /^#define CUOPT_/ { + name = $2 + value = $3 + if (name ~ /CUOPT_INFINITY/) next + if (value ~ /^[-]?[0-9]+$/) { + printf(" public static final int %s = %s;%s", name, value, "\n") + } else if (value ~ /^'\''.'\''$/) { + printf(" public static final byte %s = %s;%s", name, value, "\n") + } else if (value ~ /^".*"$/) { + printf(" public static final String %s = %s;%s", name, value, "\n") + } + } + ' "${HEADER}" + echo "}" +} > "${OUT_FILE}" diff --git a/java/cuopt/scripts/test.sh b/java/cuopt/scripts/test.sh new file mode 100644 index 0000000000..4300a29ad3 --- /dev/null +++ b/java/cuopt/scripts/test.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +MODULE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_ROOT="$(cd "${MODULE_DIR}/../.." && pwd)" +CUOPT_PREFIX="${CUOPT_PREFIX:-${CONDA_PREFIX:-${REPO_ROOT}/.cuopt_env}}" +NATIVE_BUILD_DIR="${CUOPT_JAVA_NATIVE_BUILD_DIR:-${MODULE_DIR}/build/native}" +export CUOPT_PREFIX CUOPT_JAVA_NATIVE_BUILD_DIR="${NATIVE_BUILD_DIR}" + +bash "${MODULE_DIR}/scripts/build_native.sh" + +if [[ -z "${JAVA_HOME:-}" ]]; then + JAVAC_PATH="$(command -v javac || true)" + if [[ -n "${JAVAC_PATH}" ]]; then + JAVA_HOME="$(dirname "$(dirname "$(readlink -f "${JAVAC_PATH}")")")" + export JAVA_HOME + fi +fi +if [[ ! -x "${JAVA_HOME:-}/bin/java" ]]; then + echo "JAVA_HOME must point to a JDK containing bin/java (Java 11 is required)." >&2 + exit 1 +fi + +existing_ld_library_path="${LD_LIBRARY_PATH:-}" +CUDA_RUNTIME_DIR="${CUOPT_PREFIX}/targets/x86_64-linux/lib" +library_path="${CUOPT_PREFIX}/lib:${NATIVE_BUILD_DIR}" +if [[ -d "${CUDA_RUNTIME_DIR}" ]]; then + library_path="${CUDA_RUNTIME_DIR}:${library_path}" +fi +export LD_LIBRARY_PATH="${library_path}${existing_ld_library_path:+:${existing_ld_library_path}}" + +cd "${MODULE_DIR}" +mvn verify \ + -Dcuopt.native.dir="${NATIVE_BUILD_DIR}" \ + "$@" diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/CSRMatrix.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/CSRMatrix.java new file mode 100644 index 0000000000..055ec20e0e --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/CSRMatrix.java @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.util.Arrays; + +public final class CSRMatrix { + private final int[] rowOffsets; + private final int[] columnIndices; + private final double[] values; + + /** Construct a CSR matrix using the cuOpt values, indices, offsets argument order. */ + public CSRMatrix(double[] values, int[] columnIndices, int[] rowOffsets) { + this.rowOffsets = Arrays.copyOf(rowOffsets, rowOffsets.length); + this.columnIndices = Arrays.copyOf(columnIndices, columnIndices.length); + this.values = Arrays.copyOf(values, values.length); + if (this.values.length != this.columnIndices.length) { + throw new IllegalArgumentException("CSR values and column indices must have the same length"); + } + } + + public int[] getRowOffsets() { + return Arrays.copyOf(rowOffsets, rowOffsets.length); + } + + public int[] getColumnIndices() { + return Arrays.copyOf(columnIndices, columnIndices.length); + } + + public double[] getValues() { + return Arrays.copyOf(values, values.length); + } + + int[] rowOffsetsUnsafe() { + return rowOffsets; + } + + int[] columnIndicesUnsafe() { + return columnIndices; + } + + double[] valuesUnsafe() { + return values; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Constraint.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Constraint.java new file mode 100644 index 0000000000..b6f7ef0b27 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Constraint.java @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.util.Map; + +public final class Constraint { + private int index = -1; + private LinearExpression linearExpression; + private final QuadraticExpression quadraticExpression; + private final ConstraintSense sense; + private double rhs; + private String name = ""; + private double slack = Double.NaN; + private double dualValue = Double.NaN; + + Constraint(LinearExpression expression, ConstraintSense sense, double rhs) { + this.linearExpression = expression; + this.quadraticExpression = null; + this.sense = sense; + this.rhs = rhs - expression.getConstant(); + } + + Constraint(QuadraticExpression expression, ConstraintSense sense, double rhs) { + this.linearExpression = expression.getLinearExpression(); + this.quadraticExpression = expression; + this.sense = sense; + this.rhs = rhs - expression.getLinearExpression().getConstant(); + } + + public String getConstraintName() { + return name; + } + + public int getIndex() { + return index; + } + + void setIndex(int index) { + this.index = index; + } + + Constraint setConstraintName(String name) { + this.name = name == null ? "" : name; + return this; + } + + public ConstraintSense getSense() { + return sense; + } + + public double getRHS() { + return rhs; + } + + Constraint updateLinearExpression(LinearExpression expression) { + this.linearExpression = expression; + return this; + } + + Constraint updateRHS(double rhs) { + this.rhs = rhs; + return this; + } + + public double getCoefficient(Variable variable) { + return linearExpression.getTerms().getOrDefault(variable, 0.0); + } + + public LinearExpression getLinearExpression() { + return linearExpression; + } + + public boolean isQuadratic() { + return quadraticExpression != null && !quadraticExpression.getQuadraticTerms().isEmpty(); + } + + public QuadraticExpression getQuadraticExpression() { + return quadraticExpression; + } + + public double computeSlack() { + double lhs = 0.0; + for (Map.Entry entry : linearExpression.getTerms().entrySet()) { + lhs += entry.getValue() * entry.getKey().getValue(); + } + if (isQuadratic()) { + for (QuadraticExpression.QuadraticTerm term : quadraticExpression.getQuadraticTerms()) { + lhs += + term.getCoefficient() + * term.getFirst().getValue() + * term.getSecond().getValue(); + } + } + // Match Python's Constraint.compute_slack contract: RHS minus the evaluated LHS for + // every row sense. This intentionally keeps the sign convention stable for GE rows too. + return rhs - lhs; + } + + public double getSlack() { + return slack; + } + + void setSlack(double slack) { + this.slack = slack; + } + + public double getDualValue() { + return dualValue; + } + + void setDualValue(double dualValue) { + this.dualValue = dualValue; + } + + void resetSolvedValues() { + slack = Double.NaN; + dualValue = Double.NaN; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ConstraintSense.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ConstraintSense.java new file mode 100644 index 0000000000..7c585b10c6 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ConstraintSense.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +public enum ConstraintSense { + LE((byte) 'L'), + GE((byte) 'G'), + EQ((byte) 'E'); + + private final byte nativeValue; + + ConstraintSense(byte nativeValue) { + this.nativeValue = nativeValue; + } + + byte nativeValue() { + return nativeValue; + } + + static ConstraintSense fromNative(byte value) { + for (ConstraintSense sense : values()) { + if (sense.nativeValue == value) { + return sense; + } + } + throw new IllegalArgumentException("Unknown constraint sense: " + (char) value); + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/CuOptException.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/CuOptException.java new file mode 100644 index 0000000000..b0c1137580 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/CuOptException.java @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +public class CuOptException extends RuntimeException { + private final int statusCode; + + public CuOptException(int statusCode, String message) { + super(message); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/LPStats.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/LPStats.java new file mode 100644 index 0000000000..18090a646e --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/LPStats.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +public final class LPStats { + private final double primalResidual; + private final double dualResidual; + private final double gap; + private final int numIterations; + private final SolverMethod solvedBy; + + LPStats(double[] values) { + this.primalResidual = values[0]; + this.dualResidual = values[1]; + this.gap = values[2]; + this.numIterations = (int) values[3]; + this.solvedBy = SolverMethod.fromNative((int) values[4]); + } + + public double getPrimalResidual() { + return primalResidual; + } + + public double getDualResidual() { + return dualResidual; + } + + public double getGap() { + return gap; + } + + public int getNumIterations() { + return numIterations; + } + + public SolverMethod getSolvedBy() { + return solvedBy; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/LinearExpression.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/LinearExpression.java new file mode 100644 index 0000000000..efe58c408e --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/LinearExpression.java @@ -0,0 +1,195 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class LinearExpression implements ObjectiveExpression { + private final LinkedHashMap terms; + private final double constant; + + public LinearExpression() { + this(new LinkedHashMap<>(), 0.0); + } + + private LinearExpression(LinkedHashMap terms, double constant) { + this.terms = terms; + this.constant = constant; + } + + public static LinearExpression of(Variable variable) { + return of(variable, 1.0); + } + + public static LinearExpression of(Variable variable, double coefficient) { + return new LinearExpression().plus(variable, coefficient); + } + + public static LinearExpression ofConstant(double constant) { + return new LinearExpression(new LinkedHashMap<>(), constant); + } + + public LinearExpression plus(Variable variable) { + return plus(variable, 1.0); + } + + public LinearExpression plus(Variable variable, double coefficient) { + LinkedHashMap copy = new LinkedHashMap<>(terms); + copy.merge(variable, coefficient, Double::sum); + return new LinearExpression(copy, constant); + } + + public LinearExpression plus(LinearExpression other) { + LinkedHashMap copy = new LinkedHashMap<>(terms); + for (Map.Entry entry : other.terms.entrySet()) { + copy.merge(entry.getKey(), entry.getValue(), Double::sum); + } + return new LinearExpression(copy, constant + other.constant); + } + + public QuadraticExpression plus(QuadraticExpression other) { + return other.plus(this); + } + + public LinearExpression constant(double additionalConstant) { + return new LinearExpression(new LinkedHashMap<>(terms), constant + additionalConstant); + } + + public LinearExpression plus(double value) { + return constant(value); + } + + public LinearExpression minus(double value) { + return constant(-value); + } + + public LinearExpression minus(Variable variable) { + return plus(variable, -1.0); + } + + public LinearExpression minus(Variable variable, double coefficient) { + return plus(variable, -coefficient); + } + + public LinearExpression minus(LinearExpression other) { + return plus(other.times(-1.0)); + } + + public QuadraticExpression minus(QuadraticExpression other) { + return other.times(-1.0).plus(this); + } + + public LinearExpression times(double scalar) { + LinkedHashMap copy = new LinkedHashMap<>(); + for (Map.Entry entry : terms.entrySet()) { + copy.put(entry.getKey(), entry.getValue() * scalar); + } + return new LinearExpression(copy, constant * scalar); + } + + public LinearExpression dividedBy(double scalar) { + return times(1.0 / scalar); + } + + public Map getVariablesAndCoefficients() { + return getTerms(); + } + + public Constraint le(double rhs) { + return new Constraint(this, ConstraintSense.LE, rhs); + } + + public Constraint le(Variable variable) { + return minus(variable).le(0.0); + } + + public Constraint le(LinearExpression expression) { + return minus(expression).le(0.0); + } + + public Constraint le(QuadraticExpression expression) { + return expression.times(-1.0).plus(this).le(0.0); + } + + public Constraint ge(double rhs) { + return new Constraint(this, ConstraintSense.GE, rhs); + } + + public Constraint ge(Variable variable) { + return minus(variable).ge(0.0); + } + + public Constraint ge(LinearExpression expression) { + return minus(expression).ge(0.0); + } + + public Constraint ge(QuadraticExpression expression) { + return expression.times(-1.0).plus(this).ge(0.0); + } + + public Constraint eq(double rhs) { + return new Constraint(this, ConstraintSense.EQ, rhs); + } + + public Constraint eq(Variable variable) { + return minus(variable).eq(0.0); + } + + public Constraint eq(LinearExpression expression) { + return minus(expression).eq(0.0); + } + + public Map getTerms() { + return Collections.unmodifiableMap(terms); + } + + public List getVariables() { + return List.copyOf(terms.keySet()); + } + + public Variable getVariable(int index) { + return new ArrayList<>(terms.keySet()).get(index); + } + + public List getCoefficients() { + return List.copyOf(terms.values()); + } + + public double getCoefficient(int index) { + return new ArrayList<>(terms.values()).get(index); + } + + public double getCoefficient(Variable variable) { + return terms.getOrDefault(variable, 0.0); + } + + @Override + public double getConstant() { + return constant; + } + + @Override + public LinearExpression getLinearExpression() { + return this; + } + + @Override + public boolean isQuadratic() { + return false; + } + + @Override + public double getValue() { + double value = constant; + for (Map.Entry entry : terms.entrySet()) { + value += entry.getValue() * entry.getKey().getValue(); + } + return value; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPCallback.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPCallback.java new file mode 100644 index 0000000000..6155f6aeaa --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPCallback.java @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +/** Marker interface for callbacks registered with MIP solver settings. */ +public interface MIPCallback {} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPCallbackSolution.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPCallbackSolution.java new file mode 100644 index 0000000000..258e02053a --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPCallbackSolution.java @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.util.Arrays; + +public final class MIPCallbackSolution { + final double[] solution; + final double objectiveValue; + + public MIPCallbackSolution(double[] solution, double objectiveValue) { + this.solution = Arrays.copyOf(solution, solution.length); + this.objectiveValue = objectiveValue; + } + + public double[] getSolution() { + return Arrays.copyOf(solution, solution.length); + } + + public double getObjectiveValue() { + return objectiveValue; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPSetSolutionCallback.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPSetSolutionCallback.java new file mode 100644 index 0000000000..2e2a010d2b --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPSetSolutionCallback.java @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +@FunctionalInterface +public interface MIPSetSolutionCallback extends MIPCallback { + MIPCallbackSolution getSolution(double solutionBound, Object userData); +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPSolutionCallback.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPSolutionCallback.java new file mode 100644 index 0000000000..859d745887 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPSolutionCallback.java @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +@FunctionalInterface +public interface MIPSolutionCallback extends MIPCallback { + void onSolution(double[] solution, double objectiveValue, double solutionBound, Object userData); +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPStats.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPStats.java new file mode 100644 index 0000000000..49c4d4d660 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/MIPStats.java @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +public final class MIPStats { + private final double presolveTime; + private final double maxConstraintViolation; + private final double maxIntViolation; + private final double maxVariableBoundViolation; + private final int numNodes; + private final int numSimplexIterations; + + MIPStats(double[] values) { + this.presolveTime = values[0]; + this.maxConstraintViolation = values[1]; + this.maxIntViolation = values[2]; + this.maxVariableBoundViolation = values[3]; + this.numNodes = (int) values[4]; + this.numSimplexIterations = (int) values[5]; + } + + public double getPresolveTime() { + return presolveTime; + } + + public double getMaxConstraintViolation() { + return maxConstraintViolation; + } + + public double getMaxIntViolation() { + return maxIntViolation; + } + + public double getMaxVariableBoundViolation() { + return maxVariableBoundViolation; + } + + public int getNumNodes() { + return numNodes; + } + + public int getNumSimplexIterations() { + return numSimplexIterations; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/NativeCuOpt.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/NativeCuOpt.java new file mode 100644 index 0000000000..e862e57b79 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/NativeCuOpt.java @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.nio.file.Path; + +final class NativeCuOpt { + static { + String nativeDir = System.getProperty("cuopt.native.dir"); + if (nativeDir == null || nativeDir.isBlank()) { + System.loadLibrary("cuopt_jni"); + } else { + System.load(Path.of(nativeDir, System.mapLibraryName("cuopt_jni")).toAbsolutePath().toString()); + } + } + + private NativeCuOpt() {} + + static native int getFloatSize(); + static native String[] getSolverSettingNames(); + static native long parseMPSProblem(String path, boolean fixedMPSFormat); + static native long readProblemWithFormat(String path, boolean fixedMPSFormat); + + static native long createSolverSettings(); + static native void destroySolverSettings(long handle); + static native void setSetting(long handle, String name, String value); + static native void setIntegerSetting(long handle, String name, int value); + static native void setFloatSetting(long handle, String name, double value); + static native String getSetting(long handle, String name); + static native void loadSettingsFromFile(long handle, String path); + static native boolean dumpSettingsToFile(long handle, String path, boolean hyperparametersOnly); + static native void addMIPStart(long handle, double[] values); + static native void registerMIPGetSolutionCallback( + long handle, MIPSolutionCallback callback, Object userData, int numVariables); + static native void registerMIPSetSolutionCallback( + long handle, MIPSetSolutionCallback callback, Object userData, int numVariables); + static native long createProblem( + int numConstraints, + int numVariables, + int objectiveSense, + double objectiveOffset, + double[] objectiveCoefficients, + int[] rowOffsets, + int[] columnIndices, + double[] values, + byte[] constraintSense, + double[] rhs, + double[] lowerBounds, + double[] upperBounds, + byte[] variableTypes); + + static native void writeProblem(long handle, String path); + static native void destroyProblem(long handle); + static native void setQuadraticObjective(long handle, int[] rows, int[] columns, double[] values); + static native void addQuadraticConstraint( + long handle, + int[] rows, + int[] columns, + double[] values, + int[] linearIndices, + double[] linearCoefficients, + byte sense, + double rhs); + + static native int getNumVariables(long handle); + static native int getNumConstraints(long handle); + static native int getNumNonZeros(long handle); + static native int getObjectiveSense(long handle); + static native double getObjectiveOffset(long handle); + static native double[] getObjectiveCoefficients(long handle); + static native Object[] getConstraintMatrix(long handle); + static native byte[] getConstraintSense(long handle); + static native double[] getConstraintRHS(long handle); + static native double[] getConstraintLowerBounds(long handle); + static native double[] getConstraintUpperBounds(long handle); + static native double[] getVariableLowerBounds(long handle); + static native double[] getVariableUpperBounds(long handle); + static native byte[] getVariableTypes(long handle); + static native void setVariableNames(long handle, String[] values); + static native void setRowNames(long handle, String[] values); + static native void setProblemName(long handle, String value); + static native double[] getQuadraticObjectiveValues(long handle); + static native int[] getQuadraticObjectiveIndices(long handle); + static native int[] getQuadraticObjectiveOffsets(long handle); + static native String[] getVariableNames(long handle); + static native String[] getRowNames(long handle); + static native String getProblemName(long handle); + static native int getProblemCategory(long handle); + static native Object[] getQuadraticConstraints(long handle); + static native long solve(long problemHandle, long settingsHandle); + + static native void destroySolution(long handle); + static native boolean solutionIsMIP(long handle); + static native int getTerminationStatus(long handle); + static native int getErrorStatus(long handle); + static native String getErrorString(long handle); + static native double[] getPrimalSolution(long handle, int size); + static native int getDualSolutionSize(long handle); + static native double[] getDualSolution(long handle, int size); + static native double[] getReducedCosts(long handle, int size); + static native double getObjectiveValue(long handle); + static native double getDualObjectiveValue(long handle); + static native double getSolveTime(long handle); + static native double getMIPGap(long handle); + static native double getSolutionBound(long handle); + static native double[] getLPStats(long handle); + static native double[] getMIPStats(long handle); +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/NativeProblem.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/NativeProblem.java new file mode 100644 index 0000000000..da8c0a30d3 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/NativeProblem.java @@ -0,0 +1,303 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.lang.ref.Cleaner; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +final class NativeProblem implements AutoCloseable { + private static final Cleaner CLEANER = Cleaner.create(); + private final NativeHandle nativeHandle; + private final Cleaner.Cleanable cleanable; + private final List quadraticConstraintNames = new ArrayList<>(); + + private NativeProblem(long handle) { + this.nativeHandle = new NativeHandle(handle); + this.cleanable = CLEANER.register(this, nativeHandle); + } + + static NativeProblem createProblem( + int numConstraints, + int numVariables, + ObjectiveSense objectiveSense, + double objectiveOffset, + double[] objectiveCoefficients, + CSRMatrix constraintMatrix, + byte[] constraintSense, + double[] rhs, + double[] variableLowerBounds, + double[] variableUpperBounds, + byte[] variableTypes) { + long handle = + NativeCuOpt.createProblem( + numConstraints, + numVariables, + objectiveSense.nativeValue(), + objectiveOffset, + Arrays.copyOf(objectiveCoefficients, objectiveCoefficients.length), + constraintMatrix.getRowOffsets(), + constraintMatrix.getColumnIndices(), + constraintMatrix.getValues(), + Arrays.copyOf(constraintSense, constraintSense.length), + Arrays.copyOf(rhs, rhs.length), + Arrays.copyOf(variableLowerBounds, variableLowerBounds.length), + Arrays.copyOf(variableUpperBounds, variableUpperBounds.length), + Arrays.copyOf(variableTypes, variableTypes.length)); + return new NativeProblem(handle); + } + + static NativeProblem read(String path, boolean fixedMPSFormat) { + return new NativeProblem(NativeCuOpt.readProblemWithFormat(path, fixedMPSFormat)); + } + + static NativeProblem parseMPS(String path, boolean fixedMPSFormat) { + return new NativeProblem(NativeCuOpt.parseMPSProblem(path, fixedMPSFormat)); + } + + long handle() { + nativeHandle.requireOpen(); + return nativeHandle.handle; + } + + int getNumVariables() { + return NativeCuOpt.getNumVariables(handle()); + } + + int getNumConstraints() { + return NativeCuOpt.getNumConstraints(handle()); + } + + int getNumNonZeros() { + return NativeCuOpt.getNumNonZeros(handle()); + } + + ObjectiveSense getObjectiveSense() { + return NativeCuOpt.getObjectiveSense(handle()) == ObjectiveSense.MAXIMIZE.nativeValue() + ? ObjectiveSense.MAXIMIZE + : ObjectiveSense.MINIMIZE; + } + + double getObjectiveOffset() { + return NativeCuOpt.getObjectiveOffset(handle()); + } + + double[] getObjectiveCoefficients() { + return NativeCuOpt.getObjectiveCoefficients(handle()); + } + + CSRMatrix getConstraintMatrix() { + Object[] matrix = NativeCuOpt.getConstraintMatrix(handle()); + return new CSRMatrix((double[]) matrix[2], (int[]) matrix[1], (int[]) matrix[0]); + } + + byte[] getConstraintSense() { + return NativeCuOpt.getConstraintSense(handle()); + } + + double[] getConstraintRHS() { + return NativeCuOpt.getConstraintRHS(handle()); + } + + double[] getConstraintLowerBounds() { + return NativeCuOpt.getConstraintLowerBounds(handle()); + } + + double[] getConstraintUpperBounds() { + return NativeCuOpt.getConstraintUpperBounds(handle()); + } + + double[] getVariableLowerBounds() { + return NativeCuOpt.getVariableLowerBounds(handle()); + } + + double[] getVariableUpperBounds() { + return NativeCuOpt.getVariableUpperBounds(handle()); + } + + byte[] getVariableTypes() { + return NativeCuOpt.getVariableTypes(handle()); + } + + NativeProblem setVariableNames(String[] variableNames) { + NativeCuOpt.setVariableNames(handle(), variableNames == null ? new String[0] : variableNames.clone()); + return this; + } + + NativeProblem setRowNames(String[] rowNames) { + NativeCuOpt.setRowNames(handle(), rowNames == null ? new String[0] : rowNames.clone()); + return this; + } + + NativeProblem setProblemName(String problemName) { + NativeCuOpt.setProblemName(handle(), problemName == null ? "" : problemName); + return this; + } + + double[] getQuadraticObjectiveValues() { + return NativeCuOpt.getQuadraticObjectiveValues(handle()); + } + + int[] getQuadraticObjectiveIndices() { + return NativeCuOpt.getQuadraticObjectiveIndices(handle()); + } + + int[] getQuadraticObjectiveOffsets() { + return NativeCuOpt.getQuadraticObjectiveOffsets(handle()); + } + + String[] getVariableNames() { + return NativeCuOpt.getVariableNames(handle()); + } + + String[] getRowNames() { + return NativeCuOpt.getRowNames(handle()); + } + + String getProblemName() { + return NativeCuOpt.getProblemName(handle()); + } + + ProblemCategory getProblemCategory() { + return ProblemCategory.fromNative(NativeCuOpt.getProblemCategory(handle())); + } + + NativeProblem setQuadraticObjective(QuadraticExpression expression) { + NativeCuOpt.setQuadraticObjective( + handle(), quadraticRows(expression), quadraticColumns(expression), quadraticValues(expression)); + return this; + } + + NativeProblem addQuadraticConstraint(Constraint constraint) { + if (!constraint.isQuadratic()) { + throw new IllegalArgumentException("Quadratic constraint requires quadratic terms"); + } + if (constraint.getSense() == ConstraintSense.EQ) { + throw new IllegalArgumentException("Equality quadratic constraints are not supported"); + } + QuadraticExpression expression = constraint.getQuadraticExpression(); + LinearExpression linear = constraint.getLinearExpression(); + int[] linearIndices = new int[linear.getTerms().size()]; + double[] linearCoefficients = new double[linear.getTerms().size()]; + int i = 0; + for (var entry : linear.getTerms().entrySet()) { + linearIndices[i] = entry.getKey().getIndex(); + linearCoefficients[i] = entry.getValue(); + i++; + } + NativeCuOpt.addQuadraticConstraint( + handle(), + quadraticRows(expression), + quadraticColumns(expression), + quadraticValues(expression), + linearIndices, + linearCoefficients, + constraint.getSense().nativeValue(), + constraint.getRHS()); + quadraticConstraintNames.add(constraint.getConstraintName()); + return this; + } + + List getQuadraticConstraints() { + Object[] nativeConstraints = NativeCuOpt.getQuadraticConstraints(handle()); + List result = new ArrayList<>(nativeConstraints.length); + for (int i = 0; i < nativeConstraints.length; i++) { + Object[] entry = (Object[]) nativeConstraints[i]; + int rowIndex = ((int[]) entry[0])[0]; + String rowName = (String) entry[1]; + if (i < quadraticConstraintNames.size() && !quadraticConstraintNames.get(i).isEmpty()) { + rowName = quadraticConstraintNames.get(i); + } + ConstraintSense sense = ConstraintSense.fromNative(((byte[]) entry[2])[0]); + double rhs = ((double[]) entry[5])[0]; + result.add( + new QuadraticConstraint( + rowIndex, + rowName, + sense, + (double[]) entry[3], + (int[]) entry[4], + rhs, + (int[]) entry[6], + (int[]) entry[7], + (double[]) entry[8])); + } + return List.copyOf(result); + } + + Solution solve(SolverSettings settings) { + SolverSettings actualSettings = settings == null ? new SolverSettings() : settings; + boolean closeSettings = settings == null; + try { + long solutionHandle = NativeCuOpt.solve(handle(), actualSettings.handle()); + return new Solution( + solutionHandle, + getNumVariables(), + getNumConstraints(), + getProblemCategory(), + getVariableNames()); + } finally { + if (closeSettings) { + actualSettings.close(); + } + } + } + + void writeMPS(String path) { + NativeCuOpt.writeProblem(handle(), path); + } + + @Override + public void close() { + cleanable.clean(); + } + + private static int[] quadraticRows(QuadraticExpression expression) { + int[] rows = new int[expression.getQuadraticTerms().size()]; + for (int i = 0; i < rows.length; i++) { + rows[i] = expression.getQuadraticTerms().get(i).getFirst().getIndex(); + } + return rows; + } + + private static int[] quadraticColumns(QuadraticExpression expression) { + int[] columns = new int[expression.getQuadraticTerms().size()]; + for (int i = 0; i < columns.length; i++) { + columns[i] = expression.getQuadraticTerms().get(i).getSecond().getIndex(); + } + return columns; + } + + private static double[] quadraticValues(QuadraticExpression expression) { + double[] values = new double[expression.getQuadraticTerms().size()]; + for (int i = 0; i < values.length; i++) { + values[i] = expression.getQuadraticTerms().get(i).getCoefficient(); + } + return values; + } + + private static final class NativeHandle implements Runnable { + private long handle; + + NativeHandle(long handle) { + this.handle = handle; + } + + void requireOpen() { + if (handle == 0) { + throw new IllegalStateException("Native problem is closed"); + } + } + + @Override + public void run() { + if (handle != 0) { + NativeCuOpt.destroyProblem(handle); + handle = 0; + } + } + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ObjectiveExpression.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ObjectiveExpression.java new file mode 100644 index 0000000000..95caba170b --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ObjectiveExpression.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +/** Common API for linear and quadratic objective expressions. */ +public interface ObjectiveExpression { + /** Return the linear portion of this expression. */ + LinearExpression getLinearExpression(); + + double getConstant(); + + double getValue(); + + boolean isQuadratic(); +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ObjectiveSense.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ObjectiveSense.java new file mode 100644 index 0000000000..b4161426f3 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ObjectiveSense.java @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +public enum ObjectiveSense { + MAXIMIZE(-1), + MINIMIZE(1); + + private final int nativeValue; + + ObjectiveSense(int nativeValue) { + this.nativeValue = nativeValue; + } + + int nativeValue() { + return nativeValue; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/PDLPSolverMode.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/PDLPSolverMode.java new file mode 100644 index 0000000000..e0bf9a771e --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/PDLPSolverMode.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +/** PDLP solver modes backed by constants generated from the C++ public header. */ +public enum PDLPSolverMode { + STABLE1(CuOptConstants.CUOPT_PDLP_SOLVER_MODE_STABLE1), + STABLE2(CuOptConstants.CUOPT_PDLP_SOLVER_MODE_STABLE2), + METHODICAL1(CuOptConstants.CUOPT_PDLP_SOLVER_MODE_METHODICAL1), + FAST1(CuOptConstants.CUOPT_PDLP_SOLVER_MODE_FAST1), + STABLE3(CuOptConstants.CUOPT_PDLP_SOLVER_MODE_STABLE3); + + private final int nativeValue; + + PDLPSolverMode(int nativeValue) { + this.nativeValue = nativeValue; + } + + public int nativeValue() { + return nativeValue; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Problem.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Problem.java new file mode 100644 index 0000000000..be201896e6 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Problem.java @@ -0,0 +1,712 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public final class Problem implements AutoCloseable { + private final String name; + private final List variables = new ArrayList<>(); + private final List constraints = new ArrayList<>(); + private LinearExpression linearObjective = new LinearExpression(); + private QuadraticExpression quadraticObjective = null; + private ObjectiveSense objectiveSense = ObjectiveSense.MINIMIZE; + private boolean objectiveSet = false; + private boolean solved = false; + private TerminationStatus status = TerminationStatus.NO_TERMINATION; + private double objectiveValue = Double.NaN; + private double solveTime = Double.NaN; + + public Problem() { + this(""); + } + + public Problem(String name) { + this.name = name == null ? "" : name; + } + + public String getName() { + return name; + } + + public Variable addVariable() { + return addVariable(0.0, Double.POSITIVE_INFINITY, 0.0, VariableType.CONTINUOUS, ""); + } + + public Variable addVariable( + double lowerBound, + double upperBound, + double objectiveCoefficient, + VariableType variableType, + String name) { + Variable variable = + new Variable( + variables.size(), lowerBound, upperBound, objectiveCoefficient, variableType, name); + variables.add(variable); + solved = false; + return variable; + } + + public Constraint addConstraint(Constraint constraint) { + return addConstraint(constraint, ""); + } + + public Constraint addConstraint(Constraint constraint, String name) { + constraint.setConstraintName(name); + constraint.setIndex(constraints.size()); + constraints.add(constraint); + solved = false; + return constraint; + } + + public Problem setObjective(LinearExpression expression, ObjectiveSense sense) { + this.linearObjective = expression; + this.quadraticObjective = null; + this.objectiveSense = sense; + this.objectiveSet = true; + syncVariableObjectiveCoefficients(expression); + solved = false; + resetSolvedValues(); + return this; + } + + public Problem setObjective(Variable variable, ObjectiveSense sense) { + return setObjective(LinearExpression.of(variable), sense); + } + + public Problem setObjective(double constant, ObjectiveSense sense) { + return setObjective(LinearExpression.ofConstant(constant), sense); + } + + public Problem setObjective(QuadraticExpression expression, ObjectiveSense sense) { + this.linearObjective = expression.getLinearExpression(); + this.quadraticObjective = expression; + this.objectiveSense = sense; + this.objectiveSet = true; + syncVariableObjectiveCoefficients(expression.getLinearExpression()); + solved = false; + resetSolvedValues(); + return this; + } + + public List getVariables() { + return List.copyOf(variables); + } + + public Variable getVariable(int index) { + return variables.get(index); + } + + public Variable getVariable(String variableName) { + for (Variable variable : variables) { + if (variable.getVariableName().equals(variableName)) { + return variable; + } + } + return null; + } + + public List getConstraints() { + return List.copyOf(constraints); + } + + public Constraint getConstraint(int index) { + return constraints.get(index); + } + + public Constraint getConstraint(String constraintName) { + for (Constraint constraint : constraints) { + if (constraint.getConstraintName().equals(constraintName)) { + return constraint; + } + } + return null; + } + + public int getNumVariables() { + return variables.size(); + } + + public int getNumConstraints() { + return constraints.size(); + } + + public boolean isMIP() { + return variables.stream().anyMatch(v -> v.getVariableType() != VariableType.CONTINUOUS); + } + + public boolean isSolved() { + return solved; + } + + public TerminationStatus getStatus() { + return status; + } + + public double getObjectiveValue() { + return objectiveValue; + } + + public double getSolveTime() { + return solveTime; + } + + public CSRMatrix getCSR() { + return buildLinearConstraintMatrix().matrix; + } + + private NativeProblem toNativeProblem() { + MatrixBuild matrixBuild = buildLinearConstraintMatrix(); + double[] objectiveCoefficients = objectiveCoefficients(); + double[] lowerBounds = new double[variables.size()]; + double[] upperBounds = new double[variables.size()]; + byte[] variableTypes = new byte[variables.size()]; + for (Variable variable : variables) { + int index = variable.getIndex(); + lowerBounds[index] = variable.getLowerBound(); + upperBounds[index] = variable.getUpperBound(); + variableTypes[index] = variable.getVariableType().nativeValue(); + } + + NativeProblem nativeProblem = + NativeProblem.createProblem( + matrixBuild.linearConstraints.size(), + variables.size(), + objectiveSense, + objectiveSet ? linearObjective.getConstant() : 0.0, + objectiveCoefficients, + matrixBuild.matrix, + matrixBuild.constraintSense, + matrixBuild.rhs, + lowerBounds, + upperBounds, + variableTypes); + + if (quadraticObjective != null && !quadraticObjective.getQuadraticTerms().isEmpty()) { + nativeProblem.setQuadraticObjective(quadraticObjective); + } + for (Constraint constraint : constraints) { + if (constraint.isQuadratic()) { + nativeProblem.addQuadraticConstraint(constraint); + } + } + String[] variableNames = new String[variables.size()]; + for (Variable variable : variables) { + variableNames[variable.getIndex()] = variable.getVariableName(); + } + String[] rowNames = new String[matrixBuild.linearConstraints.size()]; + for (int i = 0; i < matrixBuild.linearConstraints.size(); i++) { + rowNames[i] = matrixBuild.linearConstraints.get(i).getConstraintName(); + } + nativeProblem.setVariableNames(variableNames).setRowNames(rowNames).setProblemName(name); + return nativeProblem; + } + + public Solution solve() { + return solve(null); + } + + public Solution solve(SolverSettings settings) { + SolverSettings actualSettings = settings == null ? new SolverSettings() : settings; + boolean closeSettings = settings == null; + addMIPStarts(actualSettings); + try (NativeProblem nativeProblem = toNativeProblem()) { + Solution solution = nativeProblem.solve(actualSettings); + populateSolution(solution); + return solution; + } finally { + if (closeSettings) { + actualSettings.close(); + } + } + } + + public void writeMPS(String path) { + try (NativeProblem nativeProblem = toNativeProblem()) { + nativeProblem.writeMPS(path); + } + } + + public static Problem read(String path) { + return read(path, false); + } + + public static Problem read(String path, boolean fixedMPSFormat) { + try (NativeProblem nativeProblem = NativeProblem.read(path, fixedMPSFormat)) { + return fromNativeProblem(nativeProblem); + } + } + + private static Problem fromNativeProblem(NativeProblem nativeProblem) { + Problem problem = new Problem(nativeProblem.getProblemName()); + double[] lowerBounds = nativeProblem.getVariableLowerBounds(); + double[] upperBounds = nativeProblem.getVariableUpperBounds(); + byte[] variableTypes = nativeProblem.getVariableTypes(); + double[] objectiveCoefficients = nativeProblem.getObjectiveCoefficients(); + String[] variableNames = nativeProblem.getVariableNames(); + for (int i = 0; i < nativeProblem.getNumVariables(); i++) { + problem.addVariable( + lowerBounds[i], + upperBounds[i], + objectiveCoefficients[i], + VariableType.fromNative(variableTypes[i]), + variableNames.length > i && !variableNames[i].isEmpty() ? variableNames[i] : "x" + i); + } + + CSRMatrix matrix = nativeProblem.getConstraintMatrix(); + int[] rowOffsets = matrix.getRowOffsets(); + int[] columnIndices = matrix.getColumnIndices(); + double[] values = matrix.getValues(); + byte[] senses = nativeProblem.getConstraintSense(); + double[] rhs = nativeProblem.getConstraintRHS(); + double[] constraintLowerBounds = nativeProblem.getConstraintLowerBounds(); + double[] constraintUpperBounds = nativeProblem.getConstraintUpperBounds(); + String[] rowNames = nativeProblem.getRowNames(); + for (int row = 0; row < nativeProblem.getNumConstraints(); row++) { + LinearExpression expression = new LinearExpression(); + for (int p = rowOffsets[row]; p < rowOffsets[row + 1]; p++) { + expression = expression.plus(problem.getVariable(columnIndices[p]), values[p]); + } + ConstraintSense sense = ConstraintSense.fromNative(senses[row]); + if (constraintLowerBounds.length > row && constraintUpperBounds.length > row) { + if (Double.compare(constraintLowerBounds[row], constraintUpperBounds[row]) == 0) { + sense = ConstraintSense.EQ; + } else if (Double.compare(constraintLowerBounds[row], rhs[row]) == 0) { + sense = ConstraintSense.GE; + } else if (Double.compare(constraintUpperBounds[row], rhs[row]) == 0) { + sense = ConstraintSense.LE; + } + } + Constraint constraint; + switch (sense) { + case LE: + constraint = expression.le(rhs[row]); + break; + case GE: + constraint = expression.ge(rhs[row]); + break; + case EQ: + constraint = expression.eq(rhs[row]); + break; + default: + throw new IllegalStateException("Unsupported sense " + sense); + } + problem.addConstraint( + constraint, + rowNames.length > row && !rowNames[row].isEmpty() ? rowNames[row] : "c" + row); + } + + int[] qOffsets = nativeProblem.getQuadraticObjectiveOffsets(); + int[] qIndices = nativeProblem.getQuadraticObjectiveIndices(); + double[] qValues = nativeProblem.getQuadraticObjectiveValues(); + if (qValues.length == 0) { + LinearExpression objective = LinearExpression.ofConstant(nativeProblem.getObjectiveOffset()); + for (int i = 0; i < objectiveCoefficients.length; i++) { + if (objectiveCoefficients[i] != 0.0) { + objective = objective.plus(problem.getVariable(i), objectiveCoefficients[i]); + } + } + problem.setObjective(objective, nativeProblem.getObjectiveSense()); + } else { + QuadraticExpression objective = + new QuadraticExpression().constant(nativeProblem.getObjectiveOffset()); + for (int i = 0; i < objectiveCoefficients.length; i++) { + if (objectiveCoefficients[i] != 0.0) { + objective = objective.plus(problem.getVariable(i), objectiveCoefficients[i]); + } + } + for (int row = 0; row + 1 < qOffsets.length; row++) { + for (int p = qOffsets[row]; p < qOffsets[row + 1]; p++) { + objective = + objective.plus( + problem.getVariable(row), problem.getVariable(qIndices[p]), qValues[p]); + } + } + problem.setObjective(objective, nativeProblem.getObjectiveSense()); + } + + for (QuadraticConstraint quadraticConstraint : nativeProblem.getQuadraticConstraints()) { + QuadraticExpression expression = new QuadraticExpression(); + double[] linearValues = quadraticConstraint.getLinearValues(); + int[] linearIndices = quadraticConstraint.getLinearIndices(); + for (int i = 0; i < linearValues.length; i++) { + expression = expression.plus(problem.getVariable(linearIndices[i]), linearValues[i]); + } + int[] rows = quadraticConstraint.getRows(); + int[] columns = quadraticConstraint.getColumns(); + double[] quadraticValues = quadraticConstraint.getValues(); + for (int i = 0; i < quadraticValues.length; i++) { + expression = + expression.plus( + problem.getVariable(rows[i]), problem.getVariable(columns[i]), quadraticValues[i]); + } + Constraint constraint = + quadraticConstraint.getSense() == ConstraintSense.LE + ? expression.le(quadraticConstraint.getRHS()) + : expression.ge(quadraticConstraint.getRHS()); + problem.addConstraint(constraint, quadraticConstraint.getRowName()); + } + return problem; + } + + public static Problem readMPS(String path) { + return readMPS(path, false); + } + + public static Problem readMPS(String path, boolean fixedMPSFormat) { + try (NativeProblem nativeProblem = NativeProblem.parseMPS(path, fixedMPSFormat)) { + return fromNativeProblem(nativeProblem); + } + } + + @Override + public void close() { + // Problem is a Java-side model; native handles are scoped to solve/read/write calls. + } + + public void update() { + resetSolvedValues(); + } + + public void resetSolvedValues() { + variables.forEach(Variable::resetSolvedValues); + constraints.forEach(Constraint::resetSolvedValues); + solved = false; + status = TerminationStatus.NO_TERMINATION; + objectiveValue = Double.NaN; + solveTime = Double.NaN; + } + + public void updateConstraint(Constraint constraint, Map coefficients, Double rhs) { + if (!constraints.contains(constraint)) { + throw new IllegalArgumentException("Constraint does not belong to this problem"); + } + if (constraint.isQuadratic()) { + throw new IllegalArgumentException("updateConstraint applies to linear constraints only"); + } + LinearExpression expression = new LinearExpression(); + for (Map.Entry entry : constraint.getLinearExpression().getTerms().entrySet()) { + expression = expression.plus(entry.getKey(), entry.getValue()); + } + if (coefficients != null) { + for (Map.Entry entry : coefficients.entrySet()) { + expression = expression.plus(entry.getKey(), entry.getValue() - constraint.getCoefficient(entry.getKey())); + } + } + constraint.updateLinearExpression(expression); + if (rhs != null) { + constraint.updateRHS(rhs); + } + resetSolvedValues(); + } + + public void updateObjective(Map coefficients, Double constant, ObjectiveSense sense) { + if (coefficients != null) { + for (Map.Entry entry : coefficients.entrySet()) { + if (!variables.contains(entry.getKey())) { + throw new IllegalArgumentException("Objective variable does not belong to this problem"); + } + entry.getKey().setObjectiveCoefficient(entry.getValue()); + } + if (objectiveSet) { + LinearExpression updated = + LinearExpression.ofConstant(constant == null ? linearObjective.getConstant() : constant); + for (Map.Entry entry : linearObjective.getTerms().entrySet()) { + updated = updated.plus(entry.getKey(), coefficients.getOrDefault(entry.getKey(), entry.getValue())); + } + for (Map.Entry entry : coefficients.entrySet()) { + if (!linearObjective.getTerms().containsKey(entry.getKey())) { + updated = updated.plus(entry.getKey(), entry.getValue()); + } + } + linearObjective = updated; + if (quadraticObjective != null) { + QuadraticExpression updatedQuadratic = + new QuadraticExpression().constant(linearObjective.getConstant()); + for (Map.Entry entry : linearObjective.getTerms().entrySet()) { + updatedQuadratic = updatedQuadratic.plus(entry.getKey(), entry.getValue()); + } + for (QuadraticExpression.QuadraticTerm term : quadraticObjective.getQuadraticTerms()) { + updatedQuadratic = + updatedQuadratic.plus(term.getFirst(), term.getSecond(), term.getCoefficient()); + } + quadraticObjective = updatedQuadratic; + } + } else if (constant != null) { + linearObjective = LinearExpression.ofConstant(constant); + for (Variable variable : variables) { + if (variable.getObjectiveCoefficient() != 0.0) { + linearObjective = linearObjective.plus(variable, variable.getObjectiveCoefficient()); + } + } + objectiveSet = true; + } + } else if (constant != null) { + linearObjective = linearObjective.constant(constant - linearObjective.getConstant()); + if (quadraticObjective != null) { + quadraticObjective = quadraticObjective.plus(LinearExpression.ofConstant(constant - quadraticObjective.getLinearExpression().getConstant())); + } + objectiveSet = true; + } + if (sense != null) { + objectiveSense = sense; + } + resetSolvedValues(); + } + + public ObjectiveExpression getObjective() { + return quadraticObjective == null ? linearObjective : quadraticObjective; + } + + public ObjectiveSense getObjectiveSense() { + return objectiveSense; + } + + public double getObjectiveConstant() { + return objectiveSet ? linearObjective.getConstant() : 0.0; + } + + public int getNumNonZeros() { + return buildLinearConstraintMatrix().matrix.getValues().length; + } + + public List getQuadraticConstraints() { + List result = new ArrayList<>(); + for (Constraint constraint : constraints) { + if (constraint.isQuadratic()) { + result.add(constraint); + } + } + return List.copyOf(result); + } + + public CSRMatrix getQCSR() { + if (quadraticObjective == null) { + return null; + } + int n = variables.size(); + int[] offsets = new int[n + 1]; + Map> byRow = new TreeMap<>(); + for (int i = 0; i < n; i++) { + byRow.put(i, new TreeMap<>()); + } + for (QuadraticExpression.QuadraticTerm term : quadraticObjective.getQuadraticTerms()) { + byRow + .get(term.getFirst().getIndex()) + .merge(term.getSecond().getIndex(), term.getCoefficient(), Double::sum); + } + int nnz = 0; + for (int row = 0; row < n; row++) { + offsets[row] = nnz; + nnz += byRow.get(row).size(); + } + offsets[n] = nnz; + int[] columns = new int[nnz]; + double[] coefficients = new double[nnz]; + int position = 0; + for (int row = 0; row < n; row++) { + for (Map.Entry entry : byRow.get(row).entrySet()) { + columns[position] = entry.getKey(); + coefficients[position++] = entry.getValue(); + } + } + return new CSRMatrix(coefficients, columns, offsets); + } + + public List getIncumbentValues(double[] solution, List requestedVariables) { + List values = new ArrayList<>(); + for (Variable variable : requestedVariables) { + values.add(solution[variable.getIndex()]); + } + return List.copyOf(values); + } + + public List get_incumbent_values(double[] solution, List requestedVariables) { + return getIncumbentValues(solution, requestedVariables); + } + + public Problem relax() { + Map mapping = new LinkedHashMap<>(); + Problem relaxed = new Problem(name); + for (Variable variable : variables) { + mapping.put(variable, relaxed.addVariable(variable.getLowerBound(), variable.getUpperBound(), + variable.getObjectiveCoefficient(), VariableType.CONTINUOUS, variable.getVariableName())); + } + for (Constraint constraint : constraints) { + if (constraint.isQuadratic()) { + QuadraticExpression expression = new QuadraticExpression(); + for (Map.Entry entry : constraint.getLinearExpression().getTerms().entrySet()) { + expression = expression.plus(mapping.get(entry.getKey()), entry.getValue()); + } + for (QuadraticExpression.QuadraticTerm term : constraint.getQuadraticExpression().getQuadraticTerms()) { + expression = expression.plus(mapping.get(term.getFirst()), mapping.get(term.getSecond()), term.getCoefficient()); + } + relaxed.addConstraint(constraint.getSense() == ConstraintSense.LE + ? expression.le(constraint.getRHS()) : expression.ge(constraint.getRHS()), constraint.getConstraintName()); + } else { + LinearExpression expression = new LinearExpression(); + for (Map.Entry entry : constraint.getLinearExpression().getTerms().entrySet()) { + expression = expression.plus(mapping.get(entry.getKey()), entry.getValue()); + } + Constraint copy; + switch (constraint.getSense()) { + case LE: + copy = expression.le(constraint.getRHS()); + break; + case GE: + copy = expression.ge(constraint.getRHS()); + break; + case EQ: + copy = expression.eq(constraint.getRHS()); + break; + default: + throw new IllegalStateException("Unsupported constraint sense"); + } + relaxed.addConstraint(copy, constraint.getConstraintName()); + } + } + if (quadraticObjective != null) { + QuadraticExpression expression = new QuadraticExpression().constant(quadraticObjective.getLinearExpression().getConstant()); + for (Map.Entry entry : quadraticObjective.getLinearExpression().getTerms().entrySet()) { + expression = expression.plus(mapping.get(entry.getKey()), entry.getValue()); + } + for (QuadraticExpression.QuadraticTerm term : quadraticObjective.getQuadraticTerms()) { + expression = expression.plus(mapping.get(term.getFirst()), mapping.get(term.getSecond()), term.getCoefficient()); + } + relaxed.setObjective(expression, objectiveSense); + } else { + LinearExpression expression = new LinearExpression().constant(linearObjective.getConstant()); + for (Map.Entry entry : linearObjective.getTerms().entrySet()) { + expression = expression.plus(mapping.get(entry.getKey()), entry.getValue()); + } + relaxed.setObjective(expression, objectiveSense); + } + return relaxed; + } + + private void populateSolution(Solution solution) { + double[] primal = solution.getPrimalSolution(); + for (int i = 0; i < variables.size(); i++) { + variables.get(i).setValue(primal[i]); + } + if (!solution.isMIP()) { + double[] reducedCosts = solution.getReducedCost(); + for (int i = 0; i < variables.size(); i++) { + variables.get(i).setReducedCost(reducedCosts[i]); + } + double[] dual = solution.getDualSolution(); + int linearRow = 0; + for (Constraint constraint : constraints) { + if (!constraint.isQuadratic()) { + constraint.setDualValue(dual[linearRow++]); + } + } + } + for (Constraint constraint : constraints) { + constraint.setSlack(constraint.computeSlack()); + } + status = solution.getTerminationStatus(); + objectiveValue = solution.getPrimalObjective(); + solveTime = solution.getSolveTime(); + solved = true; + } + + private void addMIPStarts(SolverSettings settings) { + if (!isMIP()) { + return; + } + double[] starts = new double[variables.size()]; + boolean any = false; + for (Variable variable : variables) { + starts[variable.getIndex()] = variable.getMIPStart(); + any |= !Double.isNaN(variable.getMIPStart()); + } + if (any) { + settings.addMIPStart(starts); + } + } + + private double[] objectiveCoefficients() { + double[] coefficients = new double[variables.size()]; + if (!objectiveSet) { + for (Variable variable : variables) { + coefficients[variable.getIndex()] = variable.getObjectiveCoefficient(); + } + } else { + for (Map.Entry entry : linearObjective.getTerms().entrySet()) { + coefficients[entry.getKey().getIndex()] += entry.getValue(); + } + } + return coefficients; + } + + private void syncVariableObjectiveCoefficients(LinearExpression expression) { + for (Variable variable : variables) { + variable.setObjectiveCoefficient(0.0); + } + for (Map.Entry entry : expression.getTerms().entrySet()) { + if (!variables.contains(entry.getKey())) { + throw new IllegalArgumentException("Objective variable does not belong to this problem"); + } + entry.getKey().setObjectiveCoefficient(entry.getValue()); + } + } + + private MatrixBuild buildLinearConstraintMatrix() { + List linearConstraints = new ArrayList<>(); + for (Constraint constraint : constraints) { + if (!constraint.isQuadratic()) { + linearConstraints.add(constraint); + } + } + + int nnz = 0; + for (Constraint constraint : linearConstraints) { + nnz += constraint.getLinearExpression().getTerms().size(); + } + + int[] rowOffsets = new int[linearConstraints.size() + 1]; + int[] columnIndices = new int[nnz]; + double[] values = new double[nnz]; + byte[] senses = new byte[linearConstraints.size()]; + double[] rhs = new double[linearConstraints.size()]; + + int position = 0; + for (int row = 0; row < linearConstraints.size(); row++) { + Constraint constraint = linearConstraints.get(row); + rowOffsets[row] = position; + for (Map.Entry entry : constraint.getLinearExpression().getTerms().entrySet()) { + columnIndices[position] = entry.getKey().getIndex(); + values[position] = entry.getValue(); + position++; + } + senses[row] = constraint.getSense().nativeValue(); + rhs[row] = constraint.getRHS(); + } + rowOffsets[linearConstraints.size()] = position; + return new MatrixBuild( + new CSRMatrix(values, columnIndices, rowOffsets), linearConstraints, senses, rhs); + } + + private static final class MatrixBuild { + private final CSRMatrix matrix; + private final List linearConstraints; + private final byte[] constraintSense; + private final double[] rhs; + + private MatrixBuild( + CSRMatrix matrix, List linearConstraints, byte[] constraintSense, double[] rhs) { + this.matrix = matrix; + this.linearConstraints = linearConstraints; + this.constraintSense = Arrays.copyOf(constraintSense, constraintSense.length); + this.rhs = Arrays.copyOf(rhs, rhs.length); + } + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ProblemCategory.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ProblemCategory.java new file mode 100644 index 0000000000..945a91c1ec --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/ProblemCategory.java @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +/** Problem categories returned by cuOpt solutions. */ +public enum ProblemCategory { + LP(0), + MIP(1), + /** @deprecated Integer problems are categorized as {@link #MIP}. */ + @Deprecated(since = "26.08") + IP(2); + + private final int nativeValue; + + ProblemCategory(int nativeValue) { + this.nativeValue = nativeValue; + } + + public int nativeValue() { + return nativeValue; + } + + static ProblemCategory fromNative(int value) { + if (value == IP.nativeValue) { + return MIP; + } + for (ProblemCategory category : values()) { + if (category.nativeValue == value) { + return category; + } + } + throw new IllegalArgumentException("Unknown problem category: " + value); + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/QuadraticConstraint.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/QuadraticConstraint.java new file mode 100644 index 0000000000..dffc0afc2c --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/QuadraticConstraint.java @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.util.Arrays; + +/** Host-side representation of a quadratic constraint in COO form. */ +public final class QuadraticConstraint { + private final int rowIndex; + private final String rowName; + private final ConstraintSense sense; + private final double[] linearValues; + private final int[] linearIndices; + private final double rhs; + private final int[] rows; + private final int[] columns; + private final double[] values; + + public QuadraticConstraint( + int rowIndex, + String rowName, + ConstraintSense sense, + double[] linearValues, + int[] linearIndices, + double rhs, + int[] rows, + int[] columns, + double[] values) { + if (linearValues.length != linearIndices.length) { + throw new IllegalArgumentException("linearValues and linearIndices must have the same length"); + } + if (rows.length != columns.length || rows.length != values.length) { + throw new IllegalArgumentException("quadratic COO arrays must have the same length"); + } + if (sense == ConstraintSense.EQ) { + throw new IllegalArgumentException("Equality quadratic constraints are not supported"); + } + this.rowIndex = rowIndex; + this.rowName = rowName == null ? "" : rowName; + this.sense = sense; + this.linearValues = Arrays.copyOf(linearValues, linearValues.length); + this.linearIndices = Arrays.copyOf(linearIndices, linearIndices.length); + this.rhs = rhs; + this.rows = Arrays.copyOf(rows, rows.length); + this.columns = Arrays.copyOf(columns, columns.length); + this.values = Arrays.copyOf(values, values.length); + } + + public int getRowIndex() { + return rowIndex; + } + + public String getRowName() { + return rowName; + } + + public ConstraintSense getSense() { + return sense; + } + + public double[] getLinearValues() { + return Arrays.copyOf(linearValues, linearValues.length); + } + + public int[] getLinearIndices() { + return Arrays.copyOf(linearIndices, linearIndices.length); + } + + public double getRHS() { + return rhs; + } + + public int[] getRows() { + return Arrays.copyOf(rows, rows.length); + } + + public int[] getColumns() { + return Arrays.copyOf(columns, columns.length); + } + + public double[] getValues() { + return Arrays.copyOf(values, values.length); + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/QuadraticExpression.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/QuadraticExpression.java new file mode 100644 index 0000000000..c250ed52a2 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/QuadraticExpression.java @@ -0,0 +1,199 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class QuadraticExpression implements ObjectiveExpression { + public static final class QuadraticTerm { + private final Variable first; + private final Variable second; + private final double coefficient; + + QuadraticTerm(Variable first, Variable second, double coefficient) { + this.first = first; + this.second = second; + this.coefficient = coefficient; + } + + public Variable getFirst() { + return first; + } + + public Variable getSecond() { + return second; + } + + public double getCoefficient() { + return coefficient; + } + } + + private final LinearExpression linearExpression; + private final List quadraticTerms; + + public QuadraticExpression() { + this(new LinearExpression(), new ArrayList<>()); + } + + private QuadraticExpression(LinearExpression linearExpression, List quadraticTerms) { + this.linearExpression = linearExpression; + this.quadraticTerms = quadraticTerms; + } + + public static QuadraticExpression of(Variable first, Variable second, double coefficient) { + return new QuadraticExpression().plus(first, second, coefficient); + } + + public QuadraticExpression plus(Variable first, Variable second, double coefficient) { + List copy = new ArrayList<>(quadraticTerms); + copy.add(new QuadraticTerm(first, second, coefficient)); + return new QuadraticExpression(linearExpression, copy); + } + + public QuadraticExpression plus(Variable variable, double coefficient) { + return new QuadraticExpression(linearExpression.plus(variable, coefficient), quadraticTerms); + } + + public QuadraticExpression plus(Variable variable) { + return plus(variable, 1.0); + } + + public QuadraticExpression plus(double value) { + return constant(value); + } + + public QuadraticExpression plus(LinearExpression expression) { + return new QuadraticExpression(linearExpression.plus(expression), quadraticTerms); + } + + public QuadraticExpression plus(QuadraticExpression expression) { + List copy = new ArrayList<>(quadraticTerms); + copy.addAll(expression.quadraticTerms); + return new QuadraticExpression(linearExpression.plus(expression.linearExpression), copy); + } + + public QuadraticExpression constant(double constant) { + return new QuadraticExpression(linearExpression.constant(constant), quadraticTerms); + } + + public QuadraticExpression minus(QuadraticExpression expression) { + return plus(expression.times(-1.0)); + } + + public QuadraticExpression minus(LinearExpression expression) { + return plus(expression.times(-1.0)); + } + + public QuadraticExpression minus(Variable variable) { + return plus(variable, -1.0); + } + + public QuadraticExpression minus(double value) { + return plus(-value); + } + + public QuadraticExpression times(double scalar) { + List terms = new ArrayList<>(); + for (QuadraticTerm term : quadraticTerms) { + terms.add(new QuadraticTerm(term.first, term.second, term.coefficient * scalar)); + } + return new QuadraticExpression(linearExpression.times(scalar), terms); + } + + public QuadraticExpression dividedBy(double scalar) { + return times(1.0 / scalar); + } + + public Constraint le(double rhs) { + return new Constraint(this, ConstraintSense.LE, rhs); + } + + public Constraint le(Variable variable) { + return minus(variable).le(0.0); + } + + public Constraint le(LinearExpression expression) { + return minus(expression).le(0.0); + } + + public Constraint le(QuadraticExpression expression) { + return minus(expression).le(0.0); + } + + public Constraint ge(double rhs) { + return new Constraint(this, ConstraintSense.GE, rhs); + } + + public Constraint ge(Variable variable) { + return minus(variable).ge(0.0); + } + + public Constraint ge(LinearExpression expression) { + return minus(expression).ge(0.0); + } + + public Constraint ge(QuadraticExpression expression) { + return minus(expression).ge(0.0); + } + + @Override + public LinearExpression getLinearExpression() { + return linearExpression; + } + + @Override + public double getConstant() { + return linearExpression.getConstant(); + } + + @Override + public boolean isQuadratic() { + return true; + } + + public List getQuadraticTerms() { + return Collections.unmodifiableList(quadraticTerms); + } + + public List getVariables() { + List result = new ArrayList<>(); + for (QuadraticTerm term : quadraticTerms) { + result.add(new Variable[] {term.first, term.second}); + } + return Collections.unmodifiableList(result); + } + + public Variable getVariable1(int index) { + return quadraticTerms.get(index).first; + } + + public Variable getVariable2(int index) { + return quadraticTerms.get(index).second; + } + + public List getCoefficients() { + List result = new ArrayList<>(); + for (QuadraticTerm term : quadraticTerms) { + result.add(term.coefficient); + } + return Collections.unmodifiableList(result); + } + + public double getCoefficient(int index) { + return quadraticTerms.get(index).coefficient; + } + + @Override + public double getValue() { + double value = linearExpression.getValue(); + for (QuadraticTerm term : quadraticTerms) { + value += term.coefficient * term.first.getValue() * term.second.getValue(); + } + return value; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Solution.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Solution.java new file mode 100644 index 0000000000..2643873e90 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Solution.java @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.lang.ref.Cleaner; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class Solution implements AutoCloseable { + private static final Cleaner CLEANER = Cleaner.create(); + private final NativeHandle nativeHandle; + private final Cleaner.Cleanable cleanable; + private final int numVariables; + private final int numConstraints; + private final boolean mip; + private final ProblemCategory problemCategory; + private final String[] variableNames; + + Solution(long handle, int numVariables, int numConstraints) { + this(handle, numVariables, numConstraints, ProblemCategory.LP, new String[0]); + } + + Solution( + long handle, + int numVariables, + int numConstraints, + ProblemCategory problemCategory, + String[] variableNames) { + this.nativeHandle = new NativeHandle(handle); + this.cleanable = CLEANER.register(this, nativeHandle); + this.numVariables = numVariables; + this.numConstraints = numConstraints; + this.mip = NativeCuOpt.solutionIsMIP(handle); + this.problemCategory = problemCategory; + this.variableNames = variableNames == null ? new String[0] : Arrays.copyOf(variableNames, variableNames.length); + } + + long handle() { + nativeHandle.requireOpen(); + return nativeHandle.handle; + } + + public boolean isMIP() { + return mip; + } + + public ProblemCategory getProblemCategory() { + return problemCategory; + } + + public double[] getPrimalSolution() { + return NativeCuOpt.getPrimalSolution(handle(), numVariables); + } + + public double[] getDualSolution() { + requireLP("getDualSolution"); + return NativeCuOpt.getDualSolution(handle(), NativeCuOpt.getDualSolutionSize(handle())); + } + + public double[] getReducedCost() { + requireLP("getReducedCost"); + return NativeCuOpt.getReducedCosts(handle(), numVariables); + } + + public double getPrimalObjective() { + return NativeCuOpt.getObjectiveValue(handle()); + } + + public double getDualObjective() { + requireLP("getDualObjective"); + return NativeCuOpt.getDualObjectiveValue(handle()); + } + + public TerminationStatus getTerminationStatus() { + return TerminationStatus.fromNative(NativeCuOpt.getTerminationStatus(handle())); + } + + public String getTerminationReason() { + return getTerminationStatus().name(); + } + + public int getErrorStatus() { + return NativeCuOpt.getErrorStatus(handle()); + } + + public String getErrorMessage() { + return NativeCuOpt.getErrorString(handle()); + } + + public double getSolveTime() { + return NativeCuOpt.getSolveTime(handle()); + } + + public SolverMethod getSolvedBy() { + return mip ? SolverMethod.UNSET : getLPStats().getSolvedBy(); + } + + public boolean getSolvedByPDLP() { + return !mip && getSolvedBy() == SolverMethod.PDLP; + } + + public Map getVars() { + double[] values = getPrimalSolution(); + Map result = new LinkedHashMap<>(); + int count = Math.min(variableNames.length, values.length); + for (int i = 0; i < count; ++i) { + result.put(variableNames[i], values[i]); + } + return Collections.unmodifiableMap(result); + } + + public double getMIPGap() { + requireMIP("getMIPGap"); + return NativeCuOpt.getMIPGap(handle()); + } + + public double getSolutionBound() { + requireMIP("getSolutionBound"); + return NativeCuOpt.getSolutionBound(handle()); + } + + public LPStats getLPStats() { + requireLP("getLPStats"); + return new LPStats(NativeCuOpt.getLPStats(handle())); + } + + public MIPStats getMIPStats() { + requireMIP("getMIPStats"); + return new MIPStats(NativeCuOpt.getMIPStats(handle())); + } + + @Override + public void close() { + cleanable.clean(); + } + + private void requireLP(String method) { + if (mip) { + throw new IllegalStateException(method + " is not available for MIP solutions"); + } + } + + private void requireMIP(String method) { + if (!mip) { + throw new IllegalStateException(method + " is not available for LP solutions"); + } + } + + private static final class NativeHandle implements Runnable { + private long handle; + + NativeHandle(long handle) { + this.handle = handle; + } + + void requireOpen() { + if (handle == 0) { + throw new IllegalStateException("Solution is closed"); + } + } + + @Override + public void run() { + if (handle != 0) { + NativeCuOpt.destroySolution(handle); + handle = 0; + } + } + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/SolverMethod.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/SolverMethod.java new file mode 100644 index 0000000000..5efb053cda --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/SolverMethod.java @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +/** Solver methods backed by constants generated from the C++ public header. */ +public enum SolverMethod { + CONCURRENT(CuOptConstants.CUOPT_METHOD_CONCURRENT), + PDLP(CuOptConstants.CUOPT_METHOD_PDLP), + DUAL_SIMPLEX(CuOptConstants.CUOPT_METHOD_DUAL_SIMPLEX), + BARRIER(CuOptConstants.CUOPT_METHOD_BARRIER), + UNSET(CuOptConstants.CUOPT_METHOD_UNSET); + + private final int nativeValue; + + SolverMethod(int nativeValue) { + this.nativeValue = nativeValue; + } + + public int nativeValue() { + return nativeValue; + } + + static SolverMethod fromNative(int value) { + for (SolverMethod method : values()) { + if (method.nativeValue == value) { + return method; + } + } + return UNSET; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/SolverSettings.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/SolverSettings.java new file mode 100644 index 0000000000..7c07c612e4 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/SolverSettings.java @@ -0,0 +1,226 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import java.lang.ref.Cleaner; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class SolverSettings implements AutoCloseable { + private static final Cleaner CLEANER = Cleaner.create(); + private final NativeHandle nativeHandle; + private final Cleaner.Cleanable cleanable; + private final List mipCallbacks = new ArrayList<>(); + + public SolverSettings() { + this.nativeHandle = new NativeHandle(NativeCuOpt.createSolverSettings()); + this.cleanable = CLEANER.register(this, nativeHandle); + } + + long handle() { + nativeHandle.requireOpen(); + return nativeHandle.handle; + } + + public static Set getSolverSettingNames() { + return new LinkedHashSet<>(Arrays.asList(NativeCuOpt.getSolverSettingNames())); + } + + public static String getSolverSetting(String name) { + try (SolverSettings settings = new SolverSettings()) { + return settings.getSettingAsString(name); + } + } + + public static T getSolverSetting(String name, Class type) { + return parseValue(getSolverSetting(name), type); + } + + public static String getSolverSettingAsString(String name) { + try (SolverSettings settings = new SolverSettings()) { + return settings.getSettingAsString(name); + } + } + + public SolverSettings setSetting(String name, String value) { + NativeCuOpt.setSetting(handle(), name, value); + return this; + } + + public SolverSettings setSetting(String name, int value) { + NativeCuOpt.setIntegerSetting(handle(), name, value); + return this; + } + + public SolverSettings setSetting(String name, double value) { + NativeCuOpt.setFloatSetting(handle(), name, value); + return this; + } + + public SolverSettings setSetting(String name, boolean value) { + NativeCuOpt.setIntegerSetting(handle(), name, value ? 1 : 0); + return this; + } + + public String getSetting(String name) { + return getSettingAsString(name); + } + + public T getSetting(String name, Class type) { + return parseValue(getSettingAsString(name), type); + } + + public String getSettingAsString(String name) { + return NativeCuOpt.getSetting(handle(), name); + } + + public SolverSettings setMethod(SolverMethod method) { + return setSetting(CuOptConstants.CUOPT_METHOD, method.nativeValue()); + } + + public SolverSettings setPDLPSolverMode(PDLPSolverMode mode) { + return setSetting(CuOptConstants.CUOPT_PDLP_SOLVER_MODE, mode.nativeValue()); + } + + public SolverSettings setOptimalityTolerance(double tolerance) { + for (String setting : getSolverSettingNames()) { + if (setting.endsWith("tolerance") + && !setting.startsWith("mip") + && !setting.contains("infeasible")) { + setSetting(setting, tolerance); + } + } + return this; + } + + public SolverSettings addMIPStart(double[] values) { + NativeCuOpt.addMIPStart(handle(), Arrays.copyOf(values, values.length)); + return this; + } + + public SolverSettings setMIPCallback( + MIPSolutionCallback callback, Object userData, int numVariables) { + NativeCuOpt.registerMIPGetSolutionCallback(handle(), callback, userData, numVariables); + mipCallbacks.add(callback); + return this; + } + + public SolverSettings setMIPCallback( + MIPSetSolutionCallback callback, Object userData, int numVariables) { + NativeCuOpt.registerMIPSetSolutionCallback(handle(), callback, userData, numVariables); + mipCallbacks.add(callback); + return this; + } + + public List getMIPCallbacks() { + return Collections.unmodifiableList(mipCallbacks); + } + + public boolean dumpSettingsToFile(String path, boolean hyperparametersOnly) { + return NativeCuOpt.dumpSettingsToFile(handle(), path, hyperparametersOnly); + } + + public boolean dumpSettingsToFile(String path) { + return dumpSettingsToFile(path, true); + } + + public SolverSettings loadSettingsFromFile(String path) { + NativeCuOpt.loadSettingsFromFile(handle(), path); + return this; + } + + public Map toDict() { + Map result = new LinkedHashMap<>(); + Map tolerances = new LinkedHashMap<>(); + for (String setting : getSolverSettingNames()) { + Object value = inferredValue(getSettingAsString(setting)); + if (setting.endsWith("tolerance")) { + tolerances.put(setting, value); + } else { + result.put(setting, value instanceof Double && ((Double) value).isInfinite() ? null : value); + } + } + result.put("tolerances", tolerances); + return Collections.unmodifiableMap(result); + } + + private static T parseValue(String value, Class type) { + if (type == String.class) { + return type.cast(value); + } + if (type == Boolean.class) { + if (!"true".equalsIgnoreCase(value) && !"false".equalsIgnoreCase(value)) { + throw new IllegalArgumentException("Setting value is not a boolean: " + value); + } + return type.cast(Boolean.valueOf(value)); + } + try { + if (type == Integer.class) { + return type.cast(Integer.valueOf(value)); + } + if (type == Double.class) { + return type.cast(Double.valueOf(value)); + } + } catch (NumberFormatException exception) { + throw new IllegalArgumentException( + "Setting value cannot be converted to " + type.getSimpleName() + ": " + value, + exception); + } + throw new IllegalArgumentException( + "Unsupported setting type: " + type.getName() + + "; use String, Boolean, Integer, or Double"); + } + + private static Object inferredValue(String value) { + if ("true".equalsIgnoreCase(value)) { + return true; + } + if ("false".equalsIgnoreCase(value)) { + return false; + } + try { + return Integer.valueOf(value); + } catch (NumberFormatException ignored) { + try { + return Double.valueOf(value); + } catch (NumberFormatException ignoredAgain) { + return value; + } + } + } + + @Override + public void close() { + cleanable.clean(); + } + + private static final class NativeHandle implements Runnable { + private long handle; + + NativeHandle(long handle) { + this.handle = handle; + } + + void requireOpen() { + if (handle == 0) { + throw new IllegalStateException("SolverSettings is closed"); + } + } + + @Override + public void run() { + if (handle != 0) { + NativeCuOpt.destroySolverSettings(handle); + handle = 0; + } + } + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/TerminationStatus.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/TerminationStatus.java new file mode 100644 index 0000000000..126418f22f --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/TerminationStatus.java @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +/** Termination statuses backed by constants generated from the C++ public header. */ +public enum TerminationStatus { + NO_TERMINATION(CuOptConstants.CUOPT_TERMINATION_STATUS_NO_TERMINATION), + OPTIMAL(CuOptConstants.CUOPT_TERMINATION_STATUS_OPTIMAL), + INFEASIBLE(CuOptConstants.CUOPT_TERMINATION_STATUS_INFEASIBLE), + UNBOUNDED(CuOptConstants.CUOPT_TERMINATION_STATUS_UNBOUNDED), + ITERATION_LIMIT(CuOptConstants.CUOPT_TERMINATION_STATUS_ITERATION_LIMIT), + TIME_LIMIT(CuOptConstants.CUOPT_TERMINATION_STATUS_TIME_LIMIT), + NUMERICAL_ERROR(CuOptConstants.CUOPT_TERMINATION_STATUS_NUMERICAL_ERROR), + PRIMAL_FEASIBLE(CuOptConstants.CUOPT_TERMINATION_STATUS_PRIMAL_FEASIBLE), + FEASIBLE_FOUND(CuOptConstants.CUOPT_TERMINATION_STATUS_FEASIBLE_FOUND), + CONCURRENT_LIMIT(CuOptConstants.CUOPT_TERMINATION_STATUS_CONCURRENT_LIMIT), + WORK_LIMIT(CuOptConstants.CUOPT_TERMINATION_STATUS_WORK_LIMIT), + UNBOUNDED_OR_INFEASIBLE( + CuOptConstants.CUOPT_TERMINATION_STATUS_UNBOUNDED_OR_INFEASIBLE), + UNKNOWN(Integer.MIN_VALUE); + + private final int nativeValue; + + TerminationStatus(int nativeValue) { + this.nativeValue = nativeValue; + } + + public int nativeValue() { + return nativeValue; + } + + static TerminationStatus fromNative(int value) { + for (TerminationStatus status : values()) { + if (status.nativeValue == value) { + return status; + } + } + return UNKNOWN; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Variable.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Variable.java new file mode 100644 index 0000000000..b3c33f4fd6 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/Variable.java @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +public final class Variable { + private final int index; + private double lowerBound; + private double upperBound; + private double objectiveCoefficient; + private VariableType variableType; + private String name; + private double value = Double.NaN; + private double reducedCost = Double.NaN; + private double mipStart = Double.NaN; + + Variable( + int index, + double lowerBound, + double upperBound, + double objectiveCoefficient, + VariableType variableType, + String name) { + this.index = index; + this.lowerBound = lowerBound; + this.upperBound = upperBound; + this.objectiveCoefficient = objectiveCoefficient; + this.variableType = variableType; + this.name = name == null ? "" : name; + } + + public int getIndex() { + return index; + } + + public double getLowerBound() { + return lowerBound; + } + + public Variable setLowerBound(double lowerBound) { + this.lowerBound = lowerBound; + return this; + } + + public double getUpperBound() { + return upperBound; + } + + public Variable setUpperBound(double upperBound) { + this.upperBound = upperBound; + return this; + } + + public double getObjectiveCoefficient() { + return objectiveCoefficient; + } + + public Variable setObjectiveCoefficient(double objectiveCoefficient) { + this.objectiveCoefficient = objectiveCoefficient; + return this; + } + + public VariableType getVariableType() { + return variableType; + } + + public Variable setVariableType(VariableType variableType) { + this.variableType = variableType; + return this; + } + + public String getVariableName() { + return name; + } + + public Variable setVariableName(String name) { + this.name = name == null ? "" : name; + return this; + } + + public double getValue() { + return value; + } + + void setValue(double value) { + this.value = value; + } + + public double getReducedCost() { + return reducedCost; + } + + void setReducedCost(double reducedCost) { + this.reducedCost = reducedCost; + } + + public double getMIPStart() { + return mipStart; + } + + public Variable setMIPStart(double mipStart) { + this.mipStart = mipStart; + return this; + } + + void resetSolvedValues() { + value = Double.NaN; + reducedCost = Double.NaN; + } +} diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/VariableType.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/VariableType.java new file mode 100644 index 0000000000..96da4c43c2 --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicalprogramming/VariableType.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +public enum VariableType { + CONTINUOUS((byte) 'C'), + INTEGER((byte) 'I'), + SEMI_CONTINUOUS((byte) 'S'); + + private final byte nativeValue; + + VariableType(byte nativeValue) { + this.nativeValue = nativeValue; + } + + byte nativeValue() { + return nativeValue; + } + + static VariableType fromNative(byte value) { + for (VariableType type : values()) { + if (type.nativeValue == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown variable type: " + (char) value); + } +} diff --git a/java/cuopt/src/main/native/cuopt_java_native_api.cpp b/java/cuopt/src/main/native/cuopt_java_native_api.cpp new file mode 100644 index 0000000000..6745386a68 --- /dev/null +++ b/java/cuopt/src/main/native/cuopt_java_native_api.cpp @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "cuopt_java_native_api.hpp" + +#include +#include + +#include +#include + +namespace { + +using namespace cuopt::linear_programming; +using java_solver_settings_t = + cuopt::linear_programming::solver_settings_t; +using java_lp_solution_t = lp_solution_interface_t; +using java_mip_solution_t = mip_solution_interface_t; + +// cuOpt's C API intentionally keeps its settings handle opaque. The first member is the +// solver_settings_t pointer; this private view avoids adding Java-only fields to the public C API. +struct settings_handle_prefix_t { + java_solver_settings_t* settings; +}; + +java_solver_settings_t* settings_from_handle(cuOptSolverSettings handle) +{ + return handle == nullptr ? nullptr : static_cast(handle)->settings; +} + +java_lp_solution_t* get_lp_solution(cuOptSolution solution) +{ + auto* view = static_cast(solution); + return view == nullptr || view->is_mip ? nullptr : view->lp_solution_interface_ptr; +} + +java_mip_solution_t* get_mip_solution(cuOptSolution solution) +{ + auto* view = static_cast(solution); + return view == nullptr || !view->is_mip ? nullptr : view->mip_solution_interface_ptr; +} + +} // namespace + +extern "C" cuopt_int_t cuOptLoadParametersFromFile(cuOptSolverSettings settings, const char* path) +{ + if (settings == nullptr || path == nullptr || path[0] == '\0') { return CUOPT_INVALID_ARGUMENT; } + try { + settings_from_handle(settings)->load_parameters_from_file(path); + } catch (const std::exception&) { + return CUOPT_INVALID_ARGUMENT; + } + return CUOPT_SUCCESS; +} + +extern "C" cuopt_int_t cuOptDumpParametersToFile(cuOptSolverSettings settings, + const char* path, + cuopt_int_t hyperparameters_only, + cuopt_int_t* dumped_successfully) +{ + if (settings == nullptr || path == nullptr || path[0] == '\0' || dumped_successfully == nullptr) { + return CUOPT_INVALID_ARGUMENT; + } + try { + *dumped_successfully = static_cast( + settings_from_handle(settings)->dump_parameters_to_file(path, hyperparameters_only != 0)); + } catch (const std::exception&) { + return CUOPT_INVALID_ARGUMENT; + } + return CUOPT_SUCCESS; +} + +extern "C" cuopt_int_t cuOptGetNumSolverParameters(cuopt_int_t* num_parameters_ptr) +{ + if (num_parameters_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } + try { + java_solver_settings_t settings; + *num_parameters_ptr = static_cast(settings.get_parameter_names().size()); + } catch (const std::exception&) { + return CUOPT_RUNTIME_ERROR; + } + return CUOPT_SUCCESS; +} + +extern "C" cuopt_int_t cuOptGetSolverParameterName(cuopt_int_t index, + cuopt_int_t parameter_name_size, + char* parameter_name) +{ + if (index < 0 || parameter_name_size <= 0 || parameter_name == nullptr) { + return CUOPT_INVALID_ARGUMENT; + } + try { + java_solver_settings_t settings; + const auto names = settings.get_parameter_names(); + if (index >= static_cast(names.size())) { return CUOPT_INVALID_ARGUMENT; } + std::snprintf(parameter_name, + static_cast(parameter_name_size), + "%s", + names[static_cast(index)].c_str()); + } catch (const std::exception&) { + return CUOPT_RUNTIME_ERROR; + } + return CUOPT_SUCCESS; +} + +extern "C" cuopt_int_t cuOptSolutionIsMIP(cuOptSolution solution, cuopt_int_t* is_mip_ptr) +{ + if (solution == nullptr || is_mip_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } + *is_mip_ptr = + static_cast(static_cast(solution)->is_mip); + return CUOPT_SUCCESS; +} + +extern "C" cuopt_int_t cuOptGetLPSolverStats(cuOptSolution solution, + cuopt_float_t* primal_residual_ptr, + cuopt_float_t* dual_residual_ptr, + cuopt_float_t* gap_ptr, + cuopt_int_t* num_iterations_ptr, + cuopt_int_t* solved_by_ptr) +{ + auto* lp = get_lp_solution(solution); + if (lp == nullptr) { return CUOPT_INVALID_ARGUMENT; } + try { + if (primal_residual_ptr != nullptr) { *primal_residual_ptr = lp->get_l2_primal_residual(); } + if (dual_residual_ptr != nullptr) { *dual_residual_ptr = lp->get_l2_dual_residual(); } + if (gap_ptr != nullptr) { *gap_ptr = lp->get_gap(); } + if (num_iterations_ptr != nullptr) { *num_iterations_ptr = lp->get_num_iterations(); } + if (solved_by_ptr != nullptr) { *solved_by_ptr = static_cast(lp->solved_by()); } + } catch (const std::exception&) { + return CUOPT_RUNTIME_ERROR; + } + return CUOPT_SUCCESS; +} + +extern "C" cuopt_int_t cuOptGetMIPSolverStats(cuOptSolution solution, + cuopt_float_t* presolve_time_ptr, + cuopt_float_t* max_constraint_violation_ptr, + cuopt_float_t* max_int_violation_ptr, + cuopt_float_t* max_variable_bound_violation_ptr, + cuopt_int_t* num_nodes_ptr, + cuopt_int_t* num_simplex_iterations_ptr) +{ + auto* mip = get_mip_solution(solution); + if (mip == nullptr) { return CUOPT_INVALID_ARGUMENT; } + try { + if (presolve_time_ptr != nullptr) { *presolve_time_ptr = mip->get_presolve_time(); } + if (max_constraint_violation_ptr != nullptr) { + *max_constraint_violation_ptr = mip->get_max_constraint_violation(); + } + if (max_int_violation_ptr != nullptr) { *max_int_violation_ptr = mip->get_max_int_violation(); } + if (max_variable_bound_violation_ptr != nullptr) { + *max_variable_bound_violation_ptr = mip->get_max_variable_bound_violation(); + } + if (num_nodes_ptr != nullptr) { *num_nodes_ptr = mip->get_num_nodes(); } + if (num_simplex_iterations_ptr != nullptr) { + *num_simplex_iterations_ptr = mip->get_num_simplex_iterations(); + } + } catch (const std::exception&) { + return CUOPT_RUNTIME_ERROR; + } + return CUOPT_SUCCESS; +} diff --git a/java/cuopt/src/main/native/cuopt_java_native_api.hpp b/java/cuopt/src/main/native/cuopt_java_native_api.hpp new file mode 100644 index 0000000000..1c22e24e1e --- /dev/null +++ b/java/cuopt/src/main/native/cuopt_java_native_api.hpp @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +extern "C" { + +cuopt_int_t cuOptLoadParametersFromFile(cuOptSolverSettings settings, const char* path); +cuopt_int_t cuOptDumpParametersToFile(cuOptSolverSettings settings, + const char* path, + cuopt_int_t hyperparameters_only, + cuopt_int_t* dumped_successfully); +cuopt_int_t cuOptGetNumSolverParameters(cuopt_int_t* num_parameters_ptr); +cuopt_int_t cuOptGetSolverParameterName(cuopt_int_t index, + cuopt_int_t parameter_name_size, + char* parameter_name); + +cuopt_int_t cuOptSolutionIsMIP(cuOptSolution solution, cuopt_int_t* is_mip_ptr); +cuopt_int_t cuOptGetLPSolverStats(cuOptSolution solution, + cuopt_float_t* primal_residual_ptr, + cuopt_float_t* dual_residual_ptr, + cuopt_float_t* gap_ptr, + cuopt_int_t* num_iterations_ptr, + cuopt_int_t* solved_by_ptr); +cuopt_int_t cuOptGetMIPSolverStats(cuOptSolution solution, + cuopt_float_t* presolve_time_ptr, + cuopt_float_t* max_constraint_violation_ptr, + cuopt_float_t* max_int_violation_ptr, + cuopt_float_t* max_variable_bound_violation_ptr, + cuopt_int_t* num_nodes_ptr, + cuopt_int_t* num_simplex_iterations_ptr); +} diff --git a/java/cuopt/src/main/native/cuopt_jni.cpp b/java/cuopt/src/main/native/cuopt_jni.cpp new file mode 100644 index 0000000000..f67d28e614 --- /dev/null +++ b/java/cuopt/src/main/native/cuopt_jni.cpp @@ -0,0 +1,1207 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "cuopt_java_native_api.hpp" + +#include + +namespace { + +JavaVM* g_jvm = nullptr; + +struct java_callback_context_t { + jobject callback{nullptr}; + jobject user_data{nullptr}; + int num_variables{0}; +}; + +std::mutex g_callback_mutex; +std::unordered_map> g_callback_contexts; + +// Problems created directly by this JNI module must be destroyed here as well. +// Passing these C++ objects to cuOptDestroyProblem in libcuopt.so crosses the +// shared-library boundary with a private wrapper type. +std::mutex g_jni_owned_problem_mutex; +std::unordered_set g_jni_owned_problem_handles; + +cuOptOptimizationProblem to_problem(jlong handle) +{ + return reinterpret_cast(handle); +} + +cuopt::linear_programming::problem_and_stream_view_t* to_problem_view(jlong handle) +{ + return reinterpret_cast(handle); +} + +cuOptSolverSettings to_settings(jlong handle) +{ + return reinterpret_cast(handle); +} + +cuOptSolution to_solution(jlong handle) +{ + return reinterpret_cast(handle); +} + +jlong from_handle(void* handle) +{ + return reinterpret_cast(handle); +} + +void remember_jni_owned_problem(void* handle) +{ + std::lock_guard lock(g_jni_owned_problem_mutex); + g_jni_owned_problem_handles.insert(from_handle(handle)); +} + +bool take_jni_owned_problem(jlong handle) +{ + std::lock_guard lock(g_jni_owned_problem_mutex); + return g_jni_owned_problem_handles.erase(handle) != 0; +} + +std::vector get_double_array(JNIEnv* env, jdoubleArray array) +{ + if (array == nullptr) { return {}; } + const jsize len = env->GetArrayLength(array); + std::vector tmp(static_cast(len)); + env->GetDoubleArrayRegion(array, 0, len, tmp.data()); + return std::vector(tmp.begin(), tmp.end()); +} + +std::vector get_int_array(JNIEnv* env, jintArray array) +{ + if (array == nullptr) { return {}; } + const jsize len = env->GetArrayLength(array); + std::vector tmp(static_cast(len)); + env->GetIntArrayRegion(array, 0, len, tmp.data()); + return std::vector(tmp.begin(), tmp.end()); +} + +std::vector get_byte_array(JNIEnv* env, jbyteArray array) +{ + if (array == nullptr) { return {}; } + const jsize len = env->GetArrayLength(array); + std::vector tmp(static_cast(len)); + env->GetByteArrayRegion(array, 0, len, tmp.data()); + return std::vector(tmp.begin(), tmp.end()); +} + +std::string get_string(JNIEnv* env, jstring value); + +std::vector get_string_array(JNIEnv* env, jobjectArray array) +{ + if (array == nullptr) { return {}; } + const jsize len = env->GetArrayLength(array); + std::vector values; + values.reserve(static_cast(len)); + for (jsize i = 0; i < len; ++i) { + values.push_back(get_string(env, static_cast(env->GetObjectArrayElement(array, i)))); + } + return values; +} + +jobjectArray to_string_array(JNIEnv* env, const std::vector& values) +{ + jclass string_class = env->FindClass("java/lang/String"); + jobjectArray result = env->NewObjectArray(static_cast(values.size()), string_class, nullptr); + for (jsize i = 0; i < static_cast(values.size()); ++i) { + env->SetObjectArrayElement(result, i, env->NewStringUTF(values[static_cast(i)].c_str())); + } + return result; +} + +jdoubleArray to_double_array(JNIEnv* env, const std::vector& values) +{ + jdoubleArray result = env->NewDoubleArray(static_cast(values.size())); + std::vector tmp(values.begin(), values.end()); + env->SetDoubleArrayRegion(result, 0, static_cast(tmp.size()), tmp.data()); + return result; +} + +jintArray to_int_array(JNIEnv* env, const std::vector& values) +{ + jintArray result = env->NewIntArray(static_cast(values.size())); + std::vector tmp(values.begin(), values.end()); + env->SetIntArrayRegion(result, 0, static_cast(tmp.size()), tmp.data()); + return result; +} + +jbyteArray to_byte_array(JNIEnv* env, const std::vector& values) +{ + jbyteArray result = env->NewByteArray(static_cast(values.size())); + std::vector tmp(values.begin(), values.end()); + env->SetByteArrayRegion(result, 0, static_cast(tmp.size()), tmp.data()); + return result; +} + +std::string get_string(JNIEnv* env, jstring value) +{ + if (value == nullptr) { return {}; } + const char* chars = env->GetStringUTFChars(value, nullptr); + std::string result(chars); + env->ReleaseStringUTFChars(value, chars); + return result; +} + +void throw_cuopt_exception(JNIEnv* env, cuopt_int_t status, const std::string& message) +{ + jclass cls = env->FindClass("com/nvidia/cuopt/mathematicalprogramming/CuOptException"); + if (cls == nullptr) { return; } + jmethodID ctor = env->GetMethodID(cls, "", "(ILjava/lang/String;)V"); + if (ctor == nullptr) { return; } + jstring msg = env->NewStringUTF(message.c_str()); + jobject ex = env->NewObject(cls, ctor, static_cast(status), msg); + env->Throw(static_cast(ex)); +} + +void throw_illegal_state(JNIEnv* env, const std::string& message) +{ + jclass cls = env->FindClass("java/lang/IllegalStateException"); + if (cls == nullptr) { return; } + env->ThrowNew(cls, message.c_str()); +} + +bool check_status(JNIEnv* env, cuopt_int_t status, const char* operation) +{ + if (status == CUOPT_SUCCESS) { return true; } + throw_cuopt_exception(env, status, std::string(operation) + " failed with status " + + std::to_string(status)); + return false; +} + +cuopt::linear_programming::lp_solution_interface_t* to_lp_solution( + JNIEnv* env, jlong handle, const char* operation) +{ + auto* solution = + reinterpret_cast(handle); + if (solution == nullptr || solution->is_mip || solution->lp_solution_interface_ptr == nullptr) { + throw_illegal_state(env, std::string(operation) + " is only available for LP solutions"); + return nullptr; + } + return solution->lp_solution_interface_ptr; +} + +template +bool run_problem_operation(JNIEnv* env, const char* operation, F&& operation_fn) +{ + try { + operation_fn(); + return true; + } catch (const std::exception& e) { + throw_cuopt_exception(env, + CUOPT_INVALID_ARGUMENT, + std::string(operation) + " failed: " + e.what()); + return false; + } +} + +JNIEnv* get_callback_env(bool& detach) +{ + detach = false; + JNIEnv* env = nullptr; + if (g_jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_8) == JNI_OK) { return env; } + if (g_jvm->AttachCurrentThread(reinterpret_cast(&env), nullptr) == JNI_OK) { + detach = true; + return env; + } + return nullptr; +} + +void cleanup_callback_contexts(JNIEnv* env, jlong settings_handle) +{ + std::vector contexts; + { + std::lock_guard lock(g_callback_mutex); + auto it = g_callback_contexts.find(settings_handle); + if (it == g_callback_contexts.end()) { return; } + contexts = std::move(it->second); + g_callback_contexts.erase(it); + } + for (auto* context : contexts) { + if (context->callback != nullptr) { env->DeleteGlobalRef(context->callback); } + if (context->user_data != nullptr) { env->DeleteGlobalRef(context->user_data); } + delete context; + } +} + +void remember_callback_context(jlong settings_handle, java_callback_context_t* context) +{ + std::lock_guard lock(g_callback_mutex); + g_callback_contexts[settings_handle].push_back(context); +} + +void mip_get_solution_callback(const cuopt_float_t* solution, + const cuopt_float_t* objective_value, + const cuopt_float_t* solution_bound, + void* user_data) +{ + auto* context = static_cast(user_data); + if (context == nullptr || context->callback == nullptr) { return; } + + bool detach = false; + JNIEnv* env = get_callback_env(detach); + if (env == nullptr) { return; } + + jclass cls = env->GetObjectClass(context->callback); + jmethodID method = + env->GetMethodID(cls, "onSolution", "([DDDLjava/lang/Object;)V"); + if (method != nullptr) { + std::vector values(solution, solution + context->num_variables); + jdoubleArray solution_array = to_double_array(env, values); + env->CallVoidMethod(context->callback, + method, + solution_array, + static_cast(*objective_value), + static_cast(*solution_bound), + context->user_data); + env->DeleteLocalRef(solution_array); + } + + if (detach) { g_jvm->DetachCurrentThread(); } +} + +void mip_set_solution_callback(cuopt_float_t* solution, + cuopt_float_t* objective_value, + const cuopt_float_t* solution_bound, + void* user_data) +{ + auto* context = static_cast(user_data); + if (context == nullptr || context->callback == nullptr) { return; } + + bool detach = false; + JNIEnv* env = get_callback_env(detach); + if (env == nullptr) { return; } + + jclass cls = env->GetObjectClass(context->callback); + jmethodID method = env->GetMethodID( + cls, "getSolution", "(DLjava/lang/Object;)Lcom/nvidia/cuopt/mathematicalprogramming/MIPCallbackSolution;"); + if (method != nullptr) { + jobject callback_solution = + env->CallObjectMethod(context->callback, method, static_cast(*solution_bound), context->user_data); + if (callback_solution != nullptr) { + jclass result_cls = env->GetObjectClass(callback_solution); + jfieldID solution_field = env->GetFieldID(result_cls, "solution", "[D"); + jfieldID objective_field = env->GetFieldID(result_cls, "objectiveValue", "D"); + if (solution_field != nullptr && objective_field != nullptr) { + auto solution_array = + static_cast(env->GetObjectField(callback_solution, solution_field)); + const auto values = get_double_array(env, solution_array); + if (values.size() == static_cast(context->num_variables)) { + std::memcpy(solution, values.data(), values.size() * sizeof(cuopt_float_t)); + *objective_value = + static_cast(env->GetDoubleField(callback_solution, objective_field)); + } + } + } + } + + if (detach) { g_jvm->DetachCurrentThread(); } +} + +} // namespace + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) +{ + g_jvm = vm; + return JNI_VERSION_1_8; +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getFloatSize(JNIEnv*, jclass) +{ + return cuOptGetFloatSize(); +} + +extern "C" JNIEXPORT jobjectArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getSolverSettingNames(JNIEnv* env, jclass) +{ + cuopt_int_t count = 0; + if (!check_status(env, cuOptGetNumSolverParameters(&count), "cuOptGetNumSolverParameters")) { + return nullptr; + } + jclass string_class = env->FindClass("java/lang/String"); + jobjectArray result = env->NewObjectArray(count, string_class, nullptr); + for (cuopt_int_t i = 0; i < count; ++i) { + char buffer[256] = {}; + if (!check_status(env, cuOptGetSolverParameterName(i, sizeof(buffer), buffer), + "cuOptGetSolverParameterName")) { + return nullptr; + } + env->SetObjectArrayElement(result, i, env->NewStringUTF(buffer)); + } + return result; +} + +extern "C" JNIEXPORT jlong JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_parseMPSProblem(JNIEnv* env, + jclass, + jstring path, + jboolean fixed_mps_format) +{ + const auto filename = get_string(env, path); + auto problem = std::make_unique( + cuopt::linear_programming::get_memory_backend_type()); + try { + auto data_model = cuopt::linear_programming::io::read_mps( + filename, static_cast(fixed_mps_format)); + cuopt::linear_programming::populate_from_mps_data_model( + problem->get_problem(), data_model); + auto* raw_problem = problem.get(); + remember_jni_owned_problem(raw_problem); + problem.release(); + return from_handle(raw_problem); + } catch (const std::exception& e) { + const cuopt_int_t status = + std::string(e.what()).find("Error opening input file") != std::string::npos + ? CUOPT_MPS_FILE_ERROR + : CUOPT_MPS_PARSE_ERROR; + throw_cuopt_exception(env, status, std::string("parseMPSProblem failed: ") + e.what()); + return 0; + } +} + +extern "C" JNIEXPORT jlong JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_readProblemWithFormat(JNIEnv* env, + jclass, + jstring path, + jboolean fixed_mps_format) +{ + const auto filename = get_string(env, path); + auto problem = std::make_unique( + cuopt::linear_programming::get_memory_backend_type()); + try { + auto data_model = cuopt::linear_programming::io::read( + filename, static_cast(fixed_mps_format)); + cuopt::linear_programming::populate_from_mps_data_model( + problem->get_problem(), data_model); + auto* raw_problem = problem.get(); + remember_jni_owned_problem(raw_problem); + problem.release(); + return from_handle(raw_problem); + } catch (const std::exception& e) { + const cuopt_int_t status = + std::string(e.what()).find("Error opening input file") != std::string::npos + ? CUOPT_MPS_FILE_ERROR + : CUOPT_MPS_PARSE_ERROR; + throw_cuopt_exception(env, status, std::string("readProblemWithFormat failed: ") + e.what()); + return 0; + } +} + +extern "C" JNIEXPORT jlong JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_createSolverSettings(JNIEnv* env, jclass) +{ + cuOptSolverSettings settings = nullptr; + if (!check_status(env, cuOptCreateSolverSettings(&settings), "cuOptCreateSolverSettings")) { + return 0; + } + return from_handle(settings); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_destroySolverSettings(JNIEnv* env, + jclass, + jlong handle) +{ + if (handle == 0) { return; } + cleanup_callback_contexts(env, handle); + cuOptSolverSettings settings = to_settings(handle); + cuOptDestroySolverSettings(&settings); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_setSetting(JNIEnv* env, + jclass, + jlong handle, + jstring name, + jstring value) +{ + const auto parameter_name = get_string(env, name); + const auto parameter_value = get_string(env, value); + check_status(env, + cuOptSetParameter(to_settings(handle), parameter_name.c_str(), parameter_value.c_str()), + "cuOptSetParameter"); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_setIntegerSetting(JNIEnv* env, + jclass, + jlong handle, + jstring name, + jint value) +{ + const auto parameter_name = get_string(env, name); + check_status(env, + cuOptSetIntegerParameter(to_settings(handle), parameter_name.c_str(), value), + "cuOptSetIntegerParameter"); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_setFloatSetting(JNIEnv* env, + jclass, + jlong handle, + jstring name, + jdouble value) +{ + const auto parameter_name = get_string(env, name); + check_status(env, + cuOptSetFloatParameter( + to_settings(handle), parameter_name.c_str(), static_cast(value)), + "cuOptSetFloatParameter"); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getSetting(JNIEnv* env, + jclass, + jlong handle, + jstring name) +{ + const auto parameter_name = get_string(env, name); + char buffer[256] = {}; + if (!check_status(env, + cuOptGetParameter(to_settings(handle), parameter_name.c_str(), sizeof(buffer), buffer), + "cuOptGetParameter")) { + return nullptr; + } + return env->NewStringUTF(buffer); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_loadSettingsFromFile(JNIEnv* env, + jclass, + jlong handle, + jstring path) +{ + const auto filename = get_string(env, path); + check_status(env, + cuOptLoadParametersFromFile(to_settings(handle), filename.c_str()), + "cuOptLoadParametersFromFile"); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_dumpSettingsToFile(JNIEnv* env, + jclass, + jlong handle, + jstring path, + jboolean hyperparameters_only) +{ + const auto filename = get_string(env, path); + cuopt_int_t dumped_successfully = 0; + if (!check_status(env, + cuOptDumpParametersToFile(to_settings(handle), + filename.c_str(), + hyperparameters_only ? 1 : 0, + &dumped_successfully), + "cuOptDumpParametersToFile")) { + return JNI_FALSE; + } + return dumped_successfully != 0 ? JNI_TRUE : JNI_FALSE; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_addMIPStart(JNIEnv* env, + jclass, + jlong handle, + jdoubleArray values) +{ + const auto data = get_double_array(env, values); + check_status(env, + cuOptAddMIPStart(to_settings(handle), data.data(), static_cast(data.size())), + "cuOptAddMIPStart"); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_registerMIPGetSolutionCallback( + JNIEnv* env, jclass, jlong handle, jobject callback, jobject user_data, jint num_variables) +{ + auto* context = new java_callback_context_t; + context->callback = env->NewGlobalRef(callback); + context->user_data = user_data == nullptr ? nullptr : env->NewGlobalRef(user_data); + context->num_variables = num_variables; + const auto status = + cuOptSetMIPGetSolutionCallback(to_settings(handle), mip_get_solution_callback, context); + if (!check_status(env, status, "cuOptSetMIPGetSolutionCallback")) { + if (context->callback != nullptr) { env->DeleteGlobalRef(context->callback); } + if (context->user_data != nullptr) { env->DeleteGlobalRef(context->user_data); } + delete context; + return; + } + remember_callback_context(handle, context); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_registerMIPSetSolutionCallback( + JNIEnv* env, jclass, jlong handle, jobject callback, jobject user_data, jint num_variables) +{ + auto* context = new java_callback_context_t; + context->callback = env->NewGlobalRef(callback); + context->user_data = user_data == nullptr ? nullptr : env->NewGlobalRef(user_data); + context->num_variables = num_variables; + const auto status = + cuOptSetMIPSetSolutionCallback(to_settings(handle), mip_set_solution_callback, context); + if (!check_status(env, status, "cuOptSetMIPSetSolutionCallback")) { + if (context->callback != nullptr) { env->DeleteGlobalRef(context->callback); } + if (context->user_data != nullptr) { env->DeleteGlobalRef(context->user_data); } + delete context; + return; + } + remember_callback_context(handle, context); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_createProblem(JNIEnv* env, + jclass, + jint num_constraints, + jint num_variables, + jint objective_sense, + jdouble objective_offset, + jdoubleArray objective_coefficients, + jintArray row_offsets, + jintArray column_indices, + jdoubleArray values, + jbyteArray constraint_sense, + jdoubleArray rhs, + jdoubleArray lower_bounds, + jdoubleArray upper_bounds, + jbyteArray variable_types) +{ + const auto obj = get_double_array(env, objective_coefficients); + const auto rows = get_int_array(env, row_offsets); + const auto cols = get_int_array(env, column_indices); + const auto coeffs = get_double_array(env, values); + const auto senses = get_byte_array(env, constraint_sense); + const auto rhs_values = get_double_array(env, rhs); + const auto lbs = get_double_array(env, lower_bounds); + const auto ubs = get_double_array(env, upper_bounds); + const auto types = get_byte_array(env, variable_types); + cuOptOptimizationProblem problem = nullptr; + if (!check_status(env, + cuOptCreateProblem(num_constraints, + num_variables, + objective_sense, + static_cast(objective_offset), + obj.data(), + rows.data(), + cols.data(), + coeffs.data(), + senses.data(), + rhs_values.data(), + lbs.data(), + ubs.data(), + types.data(), + &problem), + "cuOptCreateProblem")) { + return 0; + } + return from_handle(problem); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_writeProblem(JNIEnv* env, + jclass, + jlong handle, + jstring path) +{ + const auto filename = get_string(env, path); + check_status( + env, cuOptWriteProblem(to_problem(handle), filename.c_str(), CUOPT_FILE_FORMAT_MPS), "cuOptWriteProblem"); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_destroyProblem(JNIEnv*, jclass, jlong handle) +{ + if (handle == 0) { return; } + if (take_jni_owned_problem(handle)) { + delete to_problem_view(handle); + return; + } + cuOptOptimizationProblem problem = to_problem(handle); + cuOptDestroyProblem(&problem); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_setQuadraticObjective(JNIEnv* env, + jclass, + jlong handle, + jintArray rows, + jintArray cols, + jdoubleArray coeffs) +{ + const auto row_data = get_int_array(env, rows); + const auto col_data = get_int_array(env, cols); + const auto val_data = get_double_array(env, coeffs); + check_status(env, + cuOptSetQuadraticObjective(to_problem(handle), + static_cast(val_data.size()), + row_data.data(), + col_data.data(), + val_data.data()), + "cuOptSetQuadraticObjective"); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_addQuadraticConstraint(JNIEnv* env, + jclass, + jlong handle, + jintArray rows, + jintArray cols, + jdoubleArray coeffs, + jintArray linear_indices, + jdoubleArray linear_coeffs, + jbyte sense, + jdouble rhs) +{ + const auto row_data = get_int_array(env, rows); + const auto col_data = get_int_array(env, cols); + const auto val_data = get_double_array(env, coeffs); + const auto lin_idx = get_int_array(env, linear_indices); + const auto lin_coeff = get_double_array(env, linear_coeffs); + check_status(env, + cuOptAddQuadraticConstraint(to_problem(handle), + static_cast(val_data.size()), + row_data.data(), + col_data.data(), + val_data.data(), + static_cast(lin_coeff.size()), + lin_idx.data(), + lin_coeff.data(), + static_cast(sense), + static_cast(rhs)), + "cuOptAddQuadraticConstraint"); +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getNumVariables(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_int_t value = 0; + check_status(env, cuOptGetNumVariables(to_problem(handle), &value), "cuOptGetNumVariables"); + return value; +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getNumConstraints(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_int_t value = 0; + check_status(env, cuOptGetNumConstraints(to_problem(handle), &value), "cuOptGetNumConstraints"); + return value; +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getNumNonZeros(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_int_t value = 0; + check_status(env, cuOptGetNumNonZeros(to_problem(handle), &value), "cuOptGetNumNonZeros"); + return value; +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getObjectiveSense(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_int_t value = 0; + check_status(env, cuOptGetObjectiveSense(to_problem(handle), &value), "cuOptGetObjectiveSense"); + return value; +} + +extern "C" JNIEXPORT jdouble JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getObjectiveOffset(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_float_t value = 0; + check_status(env, cuOptGetObjectiveOffset(to_problem(handle), &value), "cuOptGetObjectiveOffset"); + return value; +} + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getObjectiveCoefficients(JNIEnv* env, + jclass, + jlong handle) +{ + const int n = Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getNumVariables(env, nullptr, handle); + std::vector values(static_cast(n)); + if (!check_status(env, + cuOptGetObjectiveCoefficients(to_problem(handle), values.data()), + "cuOptGetObjectiveCoefficients")) { + return nullptr; + } + return to_double_array(env, values); +} + +extern "C" JNIEXPORT jobjectArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getConstraintMatrix(JNIEnv* env, + jclass, + jlong handle) +{ + const int rows_size = Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getNumConstraints(env, nullptr, handle) + 1; + const int nnz = Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getNumNonZeros(env, nullptr, handle); + std::vector rows(static_cast(rows_size)); + std::vector cols(static_cast(nnz)); + std::vector values(static_cast(nnz)); + if (!check_status(env, + cuOptGetConstraintMatrix(to_problem(handle), rows.data(), cols.data(), values.data()), + "cuOptGetConstraintMatrix")) { + return nullptr; + } + jclass object_class = env->FindClass("java/lang/Object"); + jobjectArray result = env->NewObjectArray(3, object_class, nullptr); + env->SetObjectArrayElement(result, 0, to_int_array(env, rows)); + env->SetObjectArrayElement(result, 1, to_int_array(env, cols)); + env->SetObjectArrayElement(result, 2, to_double_array(env, values)); + return result; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_setVariableNames(JNIEnv* env, + jclass, + jlong handle, + jobjectArray values) +{ + const auto h_values = get_string_array(env, values); + run_problem_operation(env, "setVariableNames", [&] { + to_problem_view(handle)->get_problem()->set_variable_names(h_values); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_setRowNames(JNIEnv* env, + jclass, + jlong handle, + jobjectArray values) +{ + const auto h_values = get_string_array(env, values); + run_problem_operation(env, "setRowNames", [&] { + to_problem_view(handle)->get_problem()->set_row_names(h_values); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_setProblemName(JNIEnv* env, + jclass, + jlong handle, + jstring value) +{ + const auto name = get_string(env, value); + run_problem_operation(env, "setProblemName", [&] { + to_problem_view(handle)->get_problem()->set_problem_name(name); + }); +} + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getQuadraticObjectiveValues(JNIEnv* env, + jclass, + jlong handle) +{ + return to_double_array(env, to_problem_view(handle)->get_problem()->get_quadratic_objective_values()); +} + +extern "C" JNIEXPORT jintArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getQuadraticObjectiveIndices(JNIEnv* env, + jclass, + jlong handle) +{ + return to_int_array(env, to_problem_view(handle)->get_problem()->get_quadratic_objective_indices()); +} + +extern "C" JNIEXPORT jintArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getQuadraticObjectiveOffsets(JNIEnv* env, + jclass, + jlong handle) +{ + return to_int_array(env, to_problem_view(handle)->get_problem()->get_quadratic_objective_offsets()); +} + +extern "C" JNIEXPORT jobjectArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getVariableNames(JNIEnv* env, + jclass, + jlong handle) +{ + return to_string_array(env, to_problem_view(handle)->get_problem()->get_variable_names()); +} + +extern "C" JNIEXPORT jobjectArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getRowNames(JNIEnv* env, + jclass, + jlong handle) +{ + return to_string_array(env, to_problem_view(handle)->get_problem()->get_row_names()); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getProblemName(JNIEnv* env, + jclass, + jlong handle) +{ + return env->NewStringUTF(to_problem_view(handle)->get_problem()->get_problem_name().c_str()); +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getProblemCategory(JNIEnv* env, + jclass, + jlong handle) +{ + jint category = 0; + if (!run_problem_operation(env, "getProblemCategory", [&] { + category = static_cast(to_problem_view(handle)->get_problem()->get_problem_category()); + })) { + return 0; + } + return category; +} + +extern "C" JNIEXPORT jobjectArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getQuadraticConstraints(JNIEnv* env, + jclass, + jlong handle) +{ + const auto& constraints = to_problem_view(handle)->get_problem()->get_quadratic_constraints(); + jclass object_class = env->FindClass("java/lang/Object"); + jobjectArray result = env->NewObjectArray(static_cast(constraints.size()), object_class, nullptr); + for (jsize i = 0; i < static_cast(constraints.size()); ++i) { + const auto& constraint = constraints[static_cast(i)]; + jobjectArray entry = env->NewObjectArray(9, object_class, nullptr); + env->SetObjectArrayElement(entry, 0, to_int_array(env, {constraint.constraint_row_index})); + env->SetObjectArrayElement(entry, 1, env->NewStringUTF(constraint.constraint_row_name.c_str())); + env->SetObjectArrayElement(entry, 2, to_byte_array(env, {constraint.constraint_row_type})); + env->SetObjectArrayElement(entry, 3, to_double_array(env, constraint.linear_values)); + env->SetObjectArrayElement(entry, 4, to_int_array(env, constraint.linear_indices)); + env->SetObjectArrayElement(entry, 5, to_double_array(env, {constraint.rhs_value})); + env->SetObjectArrayElement(entry, 6, to_int_array(env, constraint.rows)); + env->SetObjectArrayElement(entry, 7, to_int_array(env, constraint.cols)); + env->SetObjectArrayElement(entry, 8, to_double_array(env, constraint.vals)); + env->SetObjectArrayElement(result, i, entry); + env->DeleteLocalRef(entry); + } + return result; +} + +#define DEFINE_DOUBLE_PROBLEM_GETTER(JAVA_NAME, C_NAME, COUNT_EXPR) \ + extern "C" JNIEXPORT jdoubleArray JNICALL \ + Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_##JAVA_NAME( \ + JNIEnv* env, jclass, jlong handle) \ + { \ + const int count = (COUNT_EXPR); \ + std::vector values(static_cast(count)); \ + if (!check_status(env, C_NAME(to_problem(handle), values.data()), #C_NAME)) { return nullptr; } \ + return to_double_array(env, values); \ + } + +#define DEFINE_BYTE_PROBLEM_GETTER(JAVA_NAME, C_NAME, COUNT_EXPR) \ + extern "C" JNIEXPORT jbyteArray JNICALL \ + Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_##JAVA_NAME( \ + JNIEnv* env, jclass, jlong handle) \ + { \ + const int count = (COUNT_EXPR); \ + std::vector values(static_cast(count)); \ + if (!check_status(env, C_NAME(to_problem(handle), values.data()), #C_NAME)) { return nullptr; } \ + return to_byte_array(env, values); \ + } + +DEFINE_DOUBLE_PROBLEM_GETTER(getConstraintRHS, + cuOptGetConstraintRightHandSide, + Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getNumConstraints(env, nullptr, handle)) + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getConstraintLowerBounds(JNIEnv* env, + jclass, + jlong handle) +{ + std::vector values; + if (!run_problem_operation(env, "getConstraintLowerBounds", [&] { + values = to_problem_view(handle)->get_problem()->get_constraint_lower_bounds_host(); + })) { + return nullptr; + } + return to_double_array(env, values); +} + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getConstraintUpperBounds(JNIEnv* env, + jclass, + jlong handle) +{ + std::vector values; + if (!run_problem_operation(env, "getConstraintUpperBounds", [&] { + values = to_problem_view(handle)->get_problem()->get_constraint_upper_bounds_host(); + })) { + return nullptr; + } + return to_double_array(env, values); +} + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getVariableLowerBounds(JNIEnv* env, + jclass, + jlong handle) +{ + std::vector values; + if (!run_problem_operation(env, "getVariableLowerBounds", [&] { + values = to_problem_view(handle)->get_problem()->get_variable_lower_bounds_host(); + })) { + return nullptr; + } + return to_double_array(env, values); +} + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getVariableUpperBounds(JNIEnv* env, + jclass, + jlong handle) +{ + std::vector values; + if (!run_problem_operation(env, "getVariableUpperBounds", [&] { + values = to_problem_view(handle)->get_problem()->get_variable_upper_bounds_host(); + })) { + return nullptr; + } + return to_double_array(env, values); +} + +DEFINE_BYTE_PROBLEM_GETTER(getConstraintSense, + cuOptGetConstraintSense, + Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getNumConstraints(env, nullptr, handle)) +DEFINE_BYTE_PROBLEM_GETTER(getVariableTypes, + cuOptGetVariableTypes, + Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getNumVariables(env, nullptr, handle)) + +#undef DEFINE_DOUBLE_PROBLEM_GETTER +#undef DEFINE_BYTE_PROBLEM_GETTER + +extern "C" JNIEXPORT jlong JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_solve(JNIEnv* env, + jclass, + jlong problem_handle, + jlong settings_handle) +{ + cuOptSolution solution = nullptr; + if (!check_status(env, + cuOptSolve(to_problem(problem_handle), to_settings(settings_handle), &solution), + "cuOptSolve")) { + return 0; + } + return from_handle(solution); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_destroySolution(JNIEnv*, jclass, jlong handle) +{ + if (handle == 0) { return; } + cuOptSolution solution = to_solution(handle); + cuOptDestroySolution(&solution); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_solutionIsMIP(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_int_t value = 0; + check_status(env, cuOptSolutionIsMIP(to_solution(handle), &value), "cuOptSolutionIsMIP"); + return static_cast(value != 0); +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getTerminationStatus(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_int_t value = 0; + check_status(env, cuOptGetTerminationStatus(to_solution(handle), &value), "cuOptGetTerminationStatus"); + return value; +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getErrorStatus(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_int_t value = 0; + check_status(env, cuOptGetErrorStatus(to_solution(handle), &value), "cuOptGetErrorStatus"); + return value; +} + +extern "C" JNIEXPORT jstring JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getErrorString(JNIEnv* env, + jclass, + jlong handle) +{ + char buffer[1024] = {}; + if (!check_status(env, cuOptGetErrorString(to_solution(handle), buffer, sizeof(buffer)), "cuOptGetErrorString")) { + return nullptr; + } + return env->NewStringUTF(buffer); +} + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getPrimalSolution(JNIEnv* env, + jclass, + jlong handle, + jint size) +{ + std::vector values(static_cast(size)); + if (!check_status(env, cuOptGetPrimalSolution(to_solution(handle), values.data()), "cuOptGetPrimalSolution")) { + return nullptr; + } + return to_double_array(env, values); +} + +extern "C" JNIEXPORT jint JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getDualSolutionSize(JNIEnv* env, + jclass, + jlong handle) +{ + auto* solution = to_lp_solution(env, handle, "getDualSolution"); + if (solution == nullptr) { return 0; } + return solution->get_dual_solution_size(); +} + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getDualSolution(JNIEnv* env, + jclass, + jlong handle, + jint) +{ + auto* solution = to_lp_solution(env, handle, "getDualSolution"); + if (solution == nullptr) { return nullptr; } + try { + return to_double_array(env, solution->get_dual_solution_host()); + } catch (const std::exception& e) { + throw_cuopt_exception( + env, CUOPT_INVALID_ARGUMENT, std::string("getDualSolution failed: ") + e.what()); + return nullptr; + } +} + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getReducedCosts(JNIEnv* env, + jclass, + jlong handle, + jint) +{ + auto* solution = to_lp_solution(env, handle, "getReducedCost"); + if (solution == nullptr) { return nullptr; } + try { + return to_double_array(env, solution->get_reduced_cost_host()); + } catch (const std::exception& e) { + throw_cuopt_exception( + env, CUOPT_INVALID_ARGUMENT, std::string("getReducedCost failed: ") + e.what()); + return nullptr; + } +} + +extern "C" JNIEXPORT jdouble JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getObjectiveValue(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_float_t value = 0; + check_status(env, cuOptGetObjectiveValue(to_solution(handle), &value), "cuOptGetObjectiveValue"); + return value; +} + +extern "C" JNIEXPORT jdouble JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getDualObjectiveValue(JNIEnv* env, + jclass, + jlong handle) +{ + auto* solution = to_lp_solution(env, handle, "getDualObjective"); + if (solution == nullptr) { return 0; } + try { + return solution->get_dual_objective_value(0); + } catch (const std::exception& e) { + throw_cuopt_exception( + env, CUOPT_INVALID_ARGUMENT, std::string("getDualObjective failed: ") + e.what()); + return 0; + } +} + +extern "C" JNIEXPORT jdouble JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getSolveTime(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_float_t value = 0; + check_status(env, cuOptGetSolveTime(to_solution(handle), &value), "cuOptGetSolveTime"); + return value; +} + +extern "C" JNIEXPORT jdouble JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getMIPGap(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_float_t value = 0; + check_status(env, cuOptGetMIPGap(to_solution(handle), &value), "cuOptGetMIPGap"); + return value; +} + +extern "C" JNIEXPORT jdouble JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getSolutionBound(JNIEnv* env, + jclass, + jlong handle) +{ + cuopt_float_t value = 0; + check_status(env, cuOptGetSolutionBound(to_solution(handle), &value), "cuOptGetSolutionBound"); + return value; +} + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getLPStats(JNIEnv* env, jclass, jlong handle) +{ + cuopt_float_t primal = 0; + cuopt_float_t dual = 0; + cuopt_float_t gap = 0; + cuopt_int_t iterations = 0; + cuopt_int_t solved_by = 0; + if (!check_status(env, + cuOptGetLPSolverStats(to_solution(handle), &primal, &dual, &gap, &iterations, &solved_by), + "cuOptGetLPSolverStats")) { + return nullptr; + } + return to_double_array(env, {primal, dual, gap, static_cast(iterations), static_cast(solved_by)}); +} + +extern "C" JNIEXPORT jdoubleArray JNICALL +Java_com_nvidia_cuopt_mathematicalprogramming_NativeCuOpt_getMIPStats(JNIEnv* env, jclass, jlong handle) +{ + cuopt_float_t presolve = 0; + cuopt_float_t max_constraint = 0; + cuopt_float_t max_int = 0; + cuopt_float_t max_bound = 0; + cuopt_int_t nodes = 0; + cuopt_int_t simplex = 0; + if (!check_status(env, + cuOptGetMIPSolverStats( + to_solution(handle), &presolve, &max_constraint, &max_int, &max_bound, &nodes, &simplex), + "cuOptGetMIPSolverStats")) { + return nullptr; + } + return to_double_array(env, {presolve, max_constraint, max_int, max_bound, static_cast(nodes), static_cast(simplex)}); +} diff --git a/java/cuopt/src/test/java/com/nvidia/cuopt/mathematicalprogramming/NativeIntegrationTest.java b/java/cuopt/src/test/java/com/nvidia/cuopt/mathematicalprogramming/NativeIntegrationTest.java new file mode 100644 index 0000000000..10e0539f00 --- /dev/null +++ b/java/cuopt/src/test/java/com/nvidia/cuopt/mathematicalprogramming/NativeIntegrationTest.java @@ -0,0 +1,175 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +final class NativeIntegrationTest { + private static void assumeNativeLibrary() { + String nativeDir = System.getProperty("cuopt.native.dir"); + Assumptions.assumeTrue(nativeDir != null && !nativeDir.isBlank(), "cuopt.native.dir is unset"); + Assumptions.assumeTrue( + Files.exists(Path.of(nativeDir, System.mapLibraryName("cuopt_jni"))), + "libcuopt_jni is not built"); + } + + @Test + void solverSettingNamesAreAvailable() { + assumeNativeLibrary(); + assertTrue(SolverSettings.getSolverSettingNames().contains(CuOptConstants.CUOPT_TIME_LIMIT)); + } + + @Test + void settingsExposeTypedValuesAndSettingsFileRoundTrip() throws Exception { + assumeNativeLibrary(); + Path file = Files.createTempFile("cuopt-java-settings-", ".cfg"); + try (SolverSettings settings = new SolverSettings()) { + settings.setSetting(CuOptConstants.CUOPT_LOG_TO_CONSOLE, false); + settings.setSetting(CuOptConstants.CUOPT_TIME_LIMIT, 12.5); + settings.setOptimalityTolerance(1.0e-6); + assertEquals( + Boolean.FALSE, + settings.getSetting(CuOptConstants.CUOPT_LOG_TO_CONSOLE, Boolean.class)); + assertEquals( + 12.5, + settings.getSetting(CuOptConstants.CUOPT_TIME_LIMIT, Double.class), + 1e-12); + assertEquals( + SolverSettings.getSolverSetting(CuOptConstants.CUOPT_TIME_LIMIT), + SolverSettings.getSolverSetting(CuOptConstants.CUOPT_TIME_LIMIT, String.class)); + assertEquals( + 1.0e-6, + settings.getSetting(CuOptConstants.CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, Double.class), + 1e-12); + assertEquals( + 12.5, + Double.parseDouble(settings.getSettingAsString(CuOptConstants.CUOPT_TIME_LIMIT)), + 1e-12); + assertTrue(settings.toDict().containsKey("tolerances")); + MIPSolutionCallback callback = (solution, objective, bound, userData) -> {}; + settings.setMIPCallback(callback, "test-user-data", 2); + assertTrue(settings.getMIPCallbacks().contains(callback)); + assertDoesNotThrow(() -> settings.dumpSettingsToFile(file.toString(), true)); + settings.loadSettingsFromFile(file.toString()); + } finally { + Files.deleteIfExists(file); + } + } + + @Test + void solvesSmallLPAndReportsStats() { + assumeNativeLibrary(); + assumeCudaDriverAvailable(); + try (Problem problem = tinyLP(); + SolverSettings settings = new SolverSettings().setMethod(SolverMethod.PDLP); + Solution solution = problem.solve(settings)) { + assertFalse(solution.isMIP()); + assertEquals(TerminationStatus.OPTIMAL, solution.getTerminationStatus()); + assertEquals(1.0, solution.getPrimalObjective(), 1e-3); + double[] primal = solution.getPrimalSolution(); + assertEquals(1.0, primal[0] + primal[1], 1e-3); + assertDoesNotThrow(solution::getLPStats); + assertThrows(IllegalStateException.class, solution::getMIPStats); + } + } + + @Test + void solvesProblemApiMIPAndLifecycleCloseIsIdempotent() { + assumeNativeLibrary(); + assumeCudaDriverAvailable(); + Problem problem = new Problem("integer"); + Variable x = problem.addVariable(0, 10, 1.0, VariableType.INTEGER, "x"); + problem.addConstraint(LinearExpression.of(x).ge(1.0)); + + try (SolverSettings settings = new SolverSettings().setSetting(CuOptConstants.CUOPT_TIME_LIMIT, 10.0); + Solution solution = problem.solve(settings)) { + assertTrue(solution.isMIP()); + assertEquals(TerminationStatus.OPTIMAL, solution.getTerminationStatus()); + assertEquals(1.0, x.getValue(), 1e-6); + assertDoesNotThrow(solution::getMIPStats); + assertThrows(IllegalStateException.class, solution::getDualSolution); + solution.close(); + solution.close(); + } + } + + @Test + void solvesSmallQP() { + assumeNativeLibrary(); + assumeCudaDriverAvailable(); + try (Problem problem = tinyLP()) { + Variable x0 = problem.getVariable(0); + Variable x1 = problem.getVariable(1); + problem.setObjective( + QuadraticExpression.of(x0, x0, 1.0).plus(x1, x1, 4.0), + ObjectiveSense.MINIMIZE); + try (SolverSettings settings = new SolverSettings().setSetting(CuOptConstants.CUOPT_ITERATION_LIMIT, 50); + Solution solution = problem.solve(settings)) { + assertFalse(solution.isMIP()); + assertDoesNotThrow(solution::getPrimalSolution); + } + } + } + + @Test + void rejectsMissingFileThroughCuOptException() { + assumeNativeLibrary(); + CuOptException exception = + assertThrows(CuOptException.class, () -> Problem.read("missing-file-does-not-exist.mps")); + assertEquals(CuOptConstants.CUOPT_MPS_FILE_ERROR, exception.getStatusCode()); + } + + @Test + void writesAndReadsMPSThroughReadAndParseMPS() throws Exception { + assumeNativeLibrary(); + assumeCudaDriverAvailable(); + Path file = Files.createTempFile("cuopt-java-roundtrip-", ".mps"); + try { + try (Problem source = tinyLP()) { + source.writeMPS(file.toString()); + } + try (Problem read = Problem.read(file.toString(), false); + Problem parsed = Problem.readMPS(file.toString(), false)) { + assertEquals(2, read.getNumVariables()); + assertEquals(1, read.getNumConstraints()); + assertEquals(read.getNumVariables(), parsed.getNumVariables()); + assertEquals(read.getNumConstraints(), parsed.getNumConstraints()); + } + } finally { + Files.deleteIfExists(file); + } + } + + private static Problem tinyLP() { + Problem problem = new Problem("tiny"); + Variable x0 = problem.addVariable(0.0, Double.POSITIVE_INFINITY, 1.0, VariableType.CONTINUOUS, "x0"); + Variable x1 = problem.addVariable(0.0, Double.POSITIVE_INFINITY, 1.0, VariableType.CONTINUOUS, "x1"); + problem.addConstraint(LinearExpression.of(x0).plus(x1).ge(1.0), "c0"); + problem.setObjective(LinearExpression.of(x0).plus(x1), ObjectiveSense.MINIMIZE); + return problem; + } + + private static void assumeCudaDriverAvailable() { + try { + Process process = new ProcessBuilder("nvidia-smi").start(); + boolean exited = process.waitFor() == 0; + Assumptions.assumeTrue(exited, "CUDA driver is unavailable"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Assumptions.assumeTrue(false, "CUDA driver check was interrupted"); + } catch (Exception e) { + Assumptions.assumeTrue(false, "CUDA driver check failed: " + e.getMessage()); + } + } +} diff --git a/java/cuopt/src/test/java/com/nvidia/cuopt/mathematicalprogramming/ProblemIntegrationTest.java b/java/cuopt/src/test/java/com/nvidia/cuopt/mathematicalprogramming/ProblemIntegrationTest.java new file mode 100644 index 0000000000..31f8f8177c --- /dev/null +++ b/java/cuopt/src/test/java/com/nvidia/cuopt/mathematicalprogramming/ProblemIntegrationTest.java @@ -0,0 +1,732 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +final class ProblemIntegrationTest { + private static final double SOLVE_TOLERANCE = 1.0e-3; + + @TestFactory + Stream problemsBuildAndSolve() { + return cases().stream() + .map(testCase -> DynamicTest.dynamicTest(testCase.name, () -> verify(testCase))); + } + + private static void verify(CaseSpec testCase) { + assumeNativeLibrary(); + assumeCudaDriverAvailable(); + + try (Problem problem = testCase.createProblem()) { + assertProblemConstruction(testCase, problem); + if (testCase.hasQuadraticObjective()) { + // QP callability is covered by NativeIntegrationTest. This case owns the independent + // Problem construction contract for quadratic objectives and constraints. + return; + } + try (SolverSettings settings = createSettings(testCase); + Solution solution = problem.solve(settings)) { + assertSolution(testCase, solution); + } + } + } + + private static void assertProblemConstruction(CaseSpec testCase, Problem problem) { + assertEquals(testCase.numVariables, problem.getNumVariables()); + assertEquals(testCase.problemConstraintCount(), problem.getNumConstraints()); + assertEquals(testCase.linearProblemConstraintCount(), problem.getCSR().getRowOffsets().length - 1); + assertEquals(testCase.objectiveSense, problem.getObjectiveSense()); + assertEquals(testCase.objectiveOffset, problem.getObjectiveConstant(), 0.0); + assertEquals(testCase.problemName, problem.getName()); + + for (int i = 0; i < testCase.numVariables; i++) { + Variable variable = problem.getVariable(i); + assertEquals(testCase.variableLowerBounds[i], variable.getLowerBound(), 0.0); + assertEquals(testCase.variableUpperBounds[i], variable.getUpperBound(), 0.0); + assertEquals(VariableType.fromNative(testCase.variableTypes[i]), variable.getVariableType()); + assertEquals(testCase.objectiveCoefficients[i], variable.getObjectiveCoefficient(), 0.0); + assertEquals(testCase.variableName(i), variable.getVariableName()); + } + + if (!testCase.isRanged()) { + CSRMatrix matrix = problem.getCSR(); + assertArrayEquals(testCase.rowOffsets, matrix.getRowOffsets()); + assertArrayEquals(testCase.columnIndices, matrix.getColumnIndices()); + assertDoubleArrayEquals(testCase.values, matrix.getValues(), 0.0); + for (int row = 0; row < testCase.numConstraints; row++) { + Constraint constraint = problem.getConstraint(row); + assertEquals(ConstraintSense.fromNative(testCase.constraintSense[row]), constraint.getSense()); + assertEquals(testCase.rhs[row], constraint.getRHS(), 0.0); + assertEquals(testCase.rowName(row), constraint.getConstraintName()); + } + } + + if (testCase.hasQuadraticObjective()) { + assertTrue(problem.getObjective().isQuadratic()); + CSRMatrix matrix = problem.getQCSR(); + assertArrayEquals(testCase.quadraticObjectiveRowOffsets, matrix.getRowOffsets()); + assertArrayEquals(testCase.quadraticObjectiveColumnIndices, matrix.getColumnIndices()); + assertDoubleArrayEquals(testCase.quadraticObjectiveValues, matrix.getValues(), 0.0); + } + + List quadraticConstraints = problem.getQuadraticConstraints(); + assertEquals(testCase.hasQuadraticConstraint() ? 1 : 0, quadraticConstraints.size()); + if (testCase.hasQuadraticConstraint()) { + Constraint constraint = quadraticConstraints.get(0); + assertEquals(testCase.quadraticConstraintName, constraint.getConstraintName()); + assertEquals(ConstraintSense.fromNative(testCase.quadraticConstraintSense), constraint.getSense()); + assertEquals(testCase.quadraticConstraintRHS, constraint.getRHS(), 0.0); + } + } + + private static void assertSolution(CaseSpec testCase, Solution solution) { + assertEquals(testCase.hasIntegerVariables(), solution.isMIP()); + assertEquals(testCase.expectedCategory(), solution.getProblemCategory()); + assertTrue( + Double.isNaN(solution.getSolveTime()) || solution.getSolveTime() >= 0.0, + "solve time must be non-negative when available"); + + if (!testCase.expectSolutionValues) { + assertTrue( + solution.getTerminationStatus() == TerminationStatus.INFEASIBLE + || solution.getTerminationStatus() == TerminationStatus.UNBOUNDED_OR_INFEASIBLE, + "expected an infeasible status, got " + solution.getTerminationStatus()); + return; + } + + assertEquals(TerminationStatus.OPTIMAL, solution.getTerminationStatus()); + double[] primal = solution.getPrimalSolution(); + assertEquals(testCase.numVariables, primal.length); + testCase.assertFeasible(primal); + + if (!Double.isNaN(testCase.expectedObjective)) { + assertEquals( + testCase.expectedObjective, + solution.getPrimalObjective(), + testCase.solutionTolerance, + "objective value"); + } + + if (testCase.hasIntegerVariables()) { + assertDoesNotThrow(solution::getMIPStats); + assertThrows(IllegalStateException.class, solution::getDualSolution); + assertThrows(IllegalStateException.class, solution::getLPStats); + } else { + assertDoesNotThrow(solution::getDualSolution); + assertDoesNotThrow(solution::getReducedCost); + assertDoesNotThrow(solution::getLPStats); + assertThrows(IllegalStateException.class, solution::getMIPStats); + } + } + + private static SolverSettings createSettings(CaseSpec testCase) { + SolverSettings settings = new SolverSettings(); + settings.setSetting(CuOptConstants.CUOPT_LOG_TO_CONSOLE, false); + settings.setSetting(CuOptConstants.CUOPT_TIME_LIMIT, 30.0); + settings.setSetting(CuOptConstants.CUOPT_RANDOM_SEED, 1); + if (testCase.hasIntegerVariables()) { + settings.setSetting( + CuOptConstants.CUOPT_MIP_DETERMINISM_MODE, CuOptConstants.CUOPT_MODE_DETERMINISTIC); + settings.setSetting(CuOptConstants.CUOPT_MIP_ABSOLUTE_GAP, 1.0e-8); + settings.setSetting(CuOptConstants.CUOPT_MIP_RELATIVE_GAP, 1.0e-8); + } else if (testCase.hasQuadraticObjective()) { + settings.setSetting(CuOptConstants.CUOPT_ITERATION_LIMIT, 50); + } else { + settings.setMethod(SolverMethod.PDLP); + settings.setPDLPSolverMode(PDLPSolverMode.STABLE1); + settings.setSetting(CuOptConstants.CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, 1.0e-7); + settings.setSetting(CuOptConstants.CUOPT_RELATIVE_PRIMAL_TOLERANCE, 1.0e-7); + settings.setSetting(CuOptConstants.CUOPT_ABSOLUTE_DUAL_TOLERANCE, 1.0e-7); + settings.setSetting(CuOptConstants.CUOPT_RELATIVE_DUAL_TOLERANCE, 1.0e-7); + settings.setSetting(CuOptConstants.CUOPT_ABSOLUTE_GAP_TOLERANCE, 1.0e-7); + settings.setSetting(CuOptConstants.CUOPT_RELATIVE_GAP_TOLERANCE, 1.0e-7); + } + return settings; + } + + private static void assumeNativeLibrary() { + String nativeDir = System.getProperty("cuopt.native.dir"); + Assumptions.assumeTrue(nativeDir != null && !nativeDir.isBlank(), "cuopt.native.dir is unset"); + Assumptions.assumeTrue( + Files.exists(Path.of(nativeDir, System.mapLibraryName("cuopt_jni"))), + "libcuopt_jni is not built"); + } + + private static void assumeCudaDriverAvailable() { + try { + Process process = new ProcessBuilder("nvidia-smi").redirectErrorStream(true).start(); + Assumptions.assumeTrue(process.waitFor() == 0, "CUDA driver is unavailable"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Assumptions.assumeTrue(false, "CUDA driver check was interrupted"); + } catch (Exception e) { + Assumptions.assumeTrue(false, "CUDA driver check failed: " + e.getMessage()); + } + } + + private static void assertDoubleArrayEquals( + double[] expected, double[] actual, double tolerance) { + assertEquals(expected.length, actual.length, "array length"); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], actual[i], tolerance, "array value at index " + i); + } + } + + private static List cases() { + return List.of( + new CaseSpec( + "lp_min_ge_unique_solution", + 1, + 2, + ObjectiveSense.MINIMIZE, + 0.25, + new double[] {1.0, 2.0}, + new int[] {0, 2}, + new int[] {0, 1}, + new double[] {1.0, 1.0}, + new byte[] {'G'}, + new double[] {3.0}, + null, + null, + new double[] {0.0, 0.0}, + new double[] {10.0, 10.0}, + new byte[] {'C', 'C'}, + true, + 3.25), + new CaseSpec( + "lp_max_le_unique_solution", + 3, + 2, + ObjectiveSense.MAXIMIZE, + -1.0, + new double[] {3.0, 2.0}, + new int[] {0, 2, 3, 4}, + new int[] {0, 1, 0, 1}, + new double[] {1.0, 1.0, 1.0, 1.0}, + new byte[] {'L', 'L', 'L'}, + new double[] {4.0, 2.0, 3.0}, + null, + null, + new double[] {0.0, 0.0}, + new double[] {10.0, 10.0}, + new byte[] {'C', 'C'}, + true, + 9.0), + new CaseSpec( + "lp_equal_with_offset", + 1, + 2, + ObjectiveSense.MINIMIZE, + 7.0, + new double[] {0.0, 1.0}, + new int[] {0, 2}, + new int[] {0, 1}, + new double[] {1.0, 1.0}, + new byte[] {'E'}, + new double[] {5.0}, + null, + null, + new double[] {0.0, 0.0}, + new double[] {5.0, 5.0}, + new byte[] {'C', 'C'}, + true, + 7.0), + new CaseSpec( + "lp_ranged_bounds", + 2, + 2, + ObjectiveSense.MINIMIZE, + 0.0, + new double[] {0.2, 1.0}, + new int[] {0, 2, 4}, + new int[] {0, 1, 0, 1}, + new double[] {1.0, 1.0, 2.0, 1.0}, + null, + null, + new double[] {1.0, 2.0}, + new double[] {3.0, 4.0}, + new double[] {0.0, 0.0}, + new double[] {10.0, 10.0}, + new byte[] {'C', 'C'}, + true, + 0.2), + new CaseSpec( + "lp_mixed_bounds_negative_coefficients", + 1, + 2, + ObjectiveSense.MINIMIZE, + -2.0, + new double[] {-1.0, 2.0}, + new int[] {0, 2}, + new int[] {0, 1}, + new double[] {1.0, 1.0}, + new byte[] {'E'}, + new double[] {1.0}, + null, + null, + new double[] {-2.0, -1.0}, + new double[] {2.0, 3.0}, + new byte[] {'C', 'C'}, + true, + -6.0), + new CaseSpec( + "lp_max_ranged_bounds", + 1, + 2, + ObjectiveSense.MAXIMIZE, + 0.0, + new double[] {2.0, 1.0}, + new int[] {0, 2}, + new int[] {0, 1}, + new double[] {1.0, 1.0}, + null, + null, + new double[] {0.0}, + new double[] {3.0}, + new double[] {0.0, 0.0}, + new double[] {2.0, 2.0}, + new byte[] {'C', 'C'}, + true, + 5.0), + new CaseSpec( + "milp_integer_unique_solution", + 1, + 2, + ObjectiveSense.MINIMIZE, + 0.0, + new double[] {1.0, 2.0}, + new int[] {0, 2}, + new int[] {0, 1}, + new double[] {1.0, 1.0}, + new byte[] {'G'}, + new double[] {2.5}, + null, + null, + new double[] {0.0, 0.0}, + new double[] {10.0, 10.0}, + new byte[] {'I', 'I'}, + true, + 3.0), + new CaseSpec( + "milp_mixed_integer_continuous_max", + 1, + 2, + ObjectiveSense.MAXIMIZE, + 0.0, + new double[] {5.0, 1.0}, + new int[] {0, 2}, + new int[] {0, 1}, + new double[] {1.0, 1.0}, + new byte[] {'L'}, + new double[] {2.5}, + null, + null, + new double[] {0.0, 0.0}, + new double[] {3.0, 10.0}, + new byte[] {'I', 'C'}, + true, + 10.5), + new CaseSpec( + "qp_diagonal_objective", + 1, + 2, + ObjectiveSense.MINIMIZE, + 0.0, + new double[] {-8.0, -16.0}, + new int[] {0, 2}, + new int[] {0, 1}, + new double[] {1.0, 1.0}, + null, + null, + new double[] {5.0}, + new double[] {1.0e20}, + new double[] {0.0, 0.0}, + new double[] {10.0, 10.0}, + new byte[] {'C', 'C'}, + true, + Double.NaN) + .withQuadraticObjective( + new int[] {0, 1, 2}, new int[] {0, 1}, new double[] {1.0, 4.0}) + .withMetadata( + new String[] {"x0", "long_variable_1"}, + new String[] {"constraint_0"}, + "qp_model") + .withQuadraticConstraint( + "qc0", + (byte) 'L', + 100.0, + new double[] {1.0}, + new int[] {0}, + new double[] {1.0}, + new int[] {0}, + new int[] {0}), + new CaseSpec( + "lp_infeasible_status", + 2, + 1, + ObjectiveSense.MINIMIZE, + 0.0, + new double[] {1.0}, + new int[] {0, 1, 2}, + new int[] {0, 0}, + new double[] {1.0, 1.0}, + new byte[] {'G', 'L'}, + new double[] {1.0, 0.0}, + null, + null, + new double[] {0.0}, + new double[] {10.0}, + new byte[] {'C'}, + false, + Double.NaN)); + } + + private static final class CaseSpec { + private final String name; + private final int numConstraints; + private final int numVariables; + private final ObjectiveSense objectiveSense; + private final double objectiveOffset; + private final double[] objectiveCoefficients; + private final int[] rowOffsets; + private final int[] columnIndices; + private final double[] values; + private final byte[] constraintSense; + private final double[] rhs; + private final double[] constraintLowerBounds; + private final double[] constraintUpperBounds; + private final double[] variableLowerBounds; + private final double[] variableUpperBounds; + private final byte[] variableTypes; + private final boolean expectSolutionValues; + private final double expectedObjective; + private final double solutionTolerance = SOLVE_TOLERANCE; + private String[] variableNames = new String[0]; + private String[] rowNames = new String[0]; + private String problemName = ""; + private String quadraticConstraintName; + private byte quadraticConstraintSense; + private double quadraticConstraintRHS; + private double[] quadraticConstraintLinearValues; + private int[] quadraticConstraintLinearIndices; + private double[] quadraticConstraintValues; + private int[] quadraticConstraintRows; + private int[] quadraticConstraintColumns; + private int[] quadraticObjectiveRowOffsets; + private int[] quadraticObjectiveColumnIndices; + private double[] quadraticObjectiveValues; + + private CaseSpec( + String name, + int numConstraints, + int numVariables, + ObjectiveSense objectiveSense, + double objectiveOffset, + double[] objectiveCoefficients, + int[] rowOffsets, + int[] columnIndices, + double[] values, + byte[] constraintSense, + double[] rhs, + double[] constraintLowerBounds, + double[] constraintUpperBounds, + double[] variableLowerBounds, + double[] variableUpperBounds, + byte[] variableTypes, + boolean expectSolutionValues, + double expectedObjective) { + this.name = name; + this.numConstraints = numConstraints; + this.numVariables = numVariables; + this.objectiveSense = objectiveSense; + this.objectiveOffset = objectiveOffset; + this.objectiveCoefficients = Arrays.copyOf(objectiveCoefficients, objectiveCoefficients.length); + this.rowOffsets = Arrays.copyOf(rowOffsets, rowOffsets.length); + this.columnIndices = Arrays.copyOf(columnIndices, columnIndices.length); + this.values = Arrays.copyOf(values, values.length); + this.constraintSense = + constraintSense == null ? null : Arrays.copyOf(constraintSense, constraintSense.length); + this.rhs = rhs == null ? null : Arrays.copyOf(rhs, rhs.length); + this.constraintLowerBounds = + constraintLowerBounds == null + ? null + : Arrays.copyOf(constraintLowerBounds, constraintLowerBounds.length); + this.constraintUpperBounds = + constraintUpperBounds == null + ? null + : Arrays.copyOf(constraintUpperBounds, constraintUpperBounds.length); + this.variableLowerBounds = Arrays.copyOf(variableLowerBounds, variableLowerBounds.length); + this.variableUpperBounds = Arrays.copyOf(variableUpperBounds, variableUpperBounds.length); + this.variableTypes = Arrays.copyOf(variableTypes, variableTypes.length); + this.expectSolutionValues = expectSolutionValues; + this.expectedObjective = expectedObjective; + } + + private Problem createProblem() { + Problem problem = new Problem(problemName); + for (int i = 0; i < numVariables; i++) { + problem.addVariable( + variableLowerBounds[i], + variableUpperBounds[i], + objectiveCoefficients[i], + VariableType.fromNative(variableTypes[i]), + variableName(i)); + } + + if (hasQuadraticObjective()) { + problem.setObjective(buildQuadraticObjective(problem), objectiveSense); + } else { + problem.setObjective(buildLinearObjective(problem), objectiveSense); + } + + for (int row = 0; row < numConstraints; row++) { + LinearExpression expression = buildRowExpression(problem, row); + if (isRanged()) { + if (!Double.isInfinite(constraintLowerBounds[row])) { + problem.addConstraint(expression.ge(constraintLowerBounds[row]), rangedRowName(row, "lower")); + } + if (!Double.isInfinite(constraintUpperBounds[row])) { + problem.addConstraint(expression.le(constraintUpperBounds[row]), rangedRowName(row, "upper")); + } + } else { + problem.addConstraint( + toConstraint(expression, ConstraintSense.fromNative(constraintSense[row]), rhs[row]), + rowName(row)); + } + } + + if (hasQuadraticConstraint()) { + QuadraticExpression expression = new QuadraticExpression(); + for (int i = 0; i < quadraticConstraintLinearValues.length; i++) { + expression = + expression.plus( + problem.getVariable(quadraticConstraintLinearIndices[i]), + quadraticConstraintLinearValues[i]); + } + for (int i = 0; i < quadraticConstraintValues.length; i++) { + expression = + expression.plus( + problem.getVariable(quadraticConstraintRows[i]), + problem.getVariable(quadraticConstraintColumns[i]), + quadraticConstraintValues[i]); + } + Constraint constraint = + ConstraintSense.fromNative(quadraticConstraintSense) == ConstraintSense.LE + ? expression.le(quadraticConstraintRHS) + : expression.ge(quadraticConstraintRHS); + problem.addConstraint(constraint, quadraticConstraintName); + } + return problem; + } + + private LinearExpression buildLinearObjective(Problem problem) { + LinearExpression objective = LinearExpression.ofConstant(objectiveOffset); + for (int i = 0; i < objectiveCoefficients.length; i++) { + if (objectiveCoefficients[i] != 0.0) { + objective = objective.plus(problem.getVariable(i), objectiveCoefficients[i]); + } + } + return objective; + } + + private QuadraticExpression buildQuadraticObjective(Problem problem) { + QuadraticExpression objective = new QuadraticExpression().constant(objectiveOffset); + for (int i = 0; i < objectiveCoefficients.length; i++) { + if (objectiveCoefficients[i] != 0.0) { + objective = objective.plus(problem.getVariable(i), objectiveCoefficients[i]); + } + } + for (int row = 0; row + 1 < quadraticObjectiveRowOffsets.length; row++) { + for (int p = quadraticObjectiveRowOffsets[row]; p < quadraticObjectiveRowOffsets[row + 1]; p++) { + objective = + objective.plus( + problem.getVariable(row), + problem.getVariable(quadraticObjectiveColumnIndices[p]), + quadraticObjectiveValues[p]); + } + } + return objective; + } + + private LinearExpression buildRowExpression(Problem problem, int row) { + LinearExpression expression = new LinearExpression(); + for (int p = rowOffsets[row]; p < rowOffsets[row + 1]; p++) { + expression = expression.plus(problem.getVariable(columnIndices[p]), values[p]); + } + return expression; + } + + private Constraint toConstraint(LinearExpression expression, ConstraintSense sense, double rhs) { + switch (sense) { + case LE: + return expression.le(rhs); + case GE: + return expression.ge(rhs); + case EQ: + return expression.eq(rhs); + default: + throw new IllegalStateException("Unsupported sense " + sense); + } + } + + private boolean isRanged() { + return constraintLowerBounds != null; + } + + private boolean hasQuadraticObjective() { + return quadraticObjectiveValues != null; + } + + private boolean hasQuadraticConstraint() { + return quadraticConstraintValues != null; + } + + private boolean hasIntegerVariables() { + for (byte type : variableTypes) { + if (type == 'I' || type == 'S') { + return true; + } + } + return false; + } + + private ProblemCategory expectedCategory() { + return hasIntegerVariables() ? ProblemCategory.MIP : ProblemCategory.LP; + } + + private int linearProblemConstraintCount() { + return isRanged() ? numConstraints * 2 : numConstraints; + } + + private int problemConstraintCount() { + return linearProblemConstraintCount() + (hasQuadraticConstraint() ? 1 : 0); + } + + private String variableName(int index) { + return variableNames.length > index ? variableNames[index] : ""; + } + + private String rowName(int index) { + return rowNames.length > index ? rowNames[index] : ""; + } + + private String rangedRowName(int index, String boundName) { + String base = rowName(index); + return base.isEmpty() ? "" : base + "_" + boundName; + } + + private CaseSpec withQuadraticObjective( + int[] rowOffsets, int[] columnIndices, double[] values) { + this.quadraticObjectiveRowOffsets = Arrays.copyOf(rowOffsets, rowOffsets.length); + this.quadraticObjectiveColumnIndices = Arrays.copyOf(columnIndices, columnIndices.length); + this.quadraticObjectiveValues = Arrays.copyOf(values, values.length); + return this; + } + + private CaseSpec withMetadata( + String[] variableNames, String[] rowNames, String problemName) { + this.variableNames = Arrays.copyOf(variableNames, variableNames.length); + this.rowNames = Arrays.copyOf(rowNames, rowNames.length); + this.problemName = problemName; + return this; + } + + private CaseSpec withQuadraticConstraint( + String name, + byte sense, + double rhs, + double[] linearValues, + int[] linearIndices, + double[] values, + int[] rows, + int[] columns) { + this.quadraticConstraintName = name; + this.quadraticConstraintSense = sense; + this.quadraticConstraintRHS = rhs; + this.quadraticConstraintLinearValues = Arrays.copyOf(linearValues, linearValues.length); + this.quadraticConstraintLinearIndices = Arrays.copyOf(linearIndices, linearIndices.length); + this.quadraticConstraintValues = Arrays.copyOf(values, values.length); + this.quadraticConstraintRows = Arrays.copyOf(rows, rows.length); + this.quadraticConstraintColumns = Arrays.copyOf(columns, columns.length); + return this; + } + + private void assertFeasible(double[] primal) { + for (int variable = 0; variable < numVariables; variable++) { + assertTrue( + primal[variable] >= variableLowerBounds[variable] - solutionTolerance, + "variable " + variable + " violates its lower bound"); + assertTrue( + primal[variable] <= variableUpperBounds[variable] + solutionTolerance, + "variable " + variable + " violates its upper bound"); + if (variableTypes[variable] == 'I') { + assertEquals( + Math.rint(primal[variable]), + primal[variable], + solutionTolerance, + "variable " + variable + " must be integral"); + } + } + + for (int row = 0; row < numConstraints; row++) { + double activity = 0.0; + for (int index = rowOffsets[row]; index < rowOffsets[row + 1]; index++) { + activity += values[index] * primal[columnIndices[index]]; + } + if (isRanged()) { + assertTrue( + activity >= constraintLowerBounds[row] - solutionTolerance, + "row " + row + " violates its lower bound"); + assertTrue( + activity <= constraintUpperBounds[row] + solutionTolerance, + "row " + row + " violates its upper bound"); + } else if (constraintSense[row] == 'L') { + assertTrue(activity <= rhs[row] + solutionTolerance, "row " + row + " violates <="); + } else if (constraintSense[row] == 'G') { + assertTrue(activity >= rhs[row] - solutionTolerance, "row " + row + " violates >="); + } else { + assertEquals(rhs[row], activity, solutionTolerance, "row " + row + " violates ="); + } + } + + if (hasQuadraticConstraint()) { + double activity = 0.0; + for (int i = 0; i < quadraticConstraintLinearValues.length; i++) { + activity += + quadraticConstraintLinearValues[i] + * primal[quadraticConstraintLinearIndices[i]]; + } + for (int i = 0; i < quadraticConstraintValues.length; i++) { + activity += + quadraticConstraintValues[i] + * primal[quadraticConstraintRows[i]] + * primal[quadraticConstraintColumns[i]]; + } + if (quadraticConstraintSense == 'L') { + assertTrue( + activity <= quadraticConstraintRHS + solutionTolerance, + "quadratic constraint violates <="); + } else { + assertTrue( + activity >= quadraticConstraintRHS - solutionTolerance, + "quadratic constraint violates >="); + } + } + } + } +} diff --git a/java/cuopt/src/test/java/com/nvidia/cuopt/mathematicalprogramming/ProblemModelingTest.java b/java/cuopt/src/test/java/com/nvidia/cuopt/mathematicalprogramming/ProblemModelingTest.java new file mode 100644 index 0000000000..dec4963ef8 --- /dev/null +++ b/java/cuopt/src/test/java/com/nvidia/cuopt/mathematicalprogramming/ProblemModelingTest.java @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicalprogramming; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +final class ProblemModelingTest { + @Test + void generatedSolverEnumsMatchCuOptConstants() { + assertEquals(CuOptConstants.CUOPT_METHOD_PDLP, SolverMethod.PDLP.nativeValue()); + assertEquals( + CuOptConstants.CUOPT_PDLP_SOLVER_MODE_STABLE1, + PDLPSolverMode.STABLE1.nativeValue()); + assertEquals( + CuOptConstants.CUOPT_TERMINATION_STATUS_OPTIMAL, + TerminationStatus.OPTIMAL.nativeValue()); + } + + @Test + void mapsLegacyIPCategoryToMIP() { + assertEquals(ProblemCategory.MIP, ProblemCategory.fromNative(2)); + } + + @Test + void buildsLinearProblemAndCSR() { + Problem problem = new Problem("Simple MIP"); + Variable x = problem.addVariable(0, Double.POSITIVE_INFINITY, 0, VariableType.INTEGER, "x"); + Variable y = problem.addVariable(10, 50, 0, VariableType.INTEGER, "y"); + + assertEquals(0, x.getIndex()); + assertEquals(1, y.getIndex()); + assertTrue(problem.isMIP()); + + problem.addConstraint(LinearExpression.of(x, 2).plus(y, 4).ge(230), "c1"); + problem.addConstraint(LinearExpression.of(x, 3).plus(y, 2).constant(10).le(200), "c2"); + problem.setObjective(LinearExpression.of(x, 5).plus(y, 3).constant(50), ObjectiveSense.MAXIMIZE); + + ObjectiveExpression objective = problem.getObjective(); + assertFalse(objective.isQuadratic()); + assertEquals(50.0, objective.getConstant()); + + CSRMatrix csr = problem.getCSR(); + assertArrayEquals(new int[] {0, 2, 4}, csr.getRowOffsets()); + assertArrayEquals(new int[] {0, 1, 0, 1}, csr.getColumnIndices()); + assertArrayEquals(new double[] {2.0, 4.0, 3.0, 2.0}, csr.getValues()); + + assertEquals(2, problem.getNumVariables()); + assertEquals(2, problem.getNumConstraints()); + assertEquals(230, problem.getConstraint(0).getRHS()); + assertEquals(190, problem.getConstraint(1).getRHS()); + } + + @Test + void duplicateLinearTermsAreMergedForSlack() { + Problem problem = new Problem(); + Variable x = problem.addVariable(); + Constraint constraint = problem.addConstraint(LinearExpression.of(x, 5).plus(x, 7).le(18)); + + x.setValue(1.0); + + assertEquals(12.0, constraint.getCoefficient(x)); + assertEquals(6.0, constraint.computeSlack()); + assertFalse(problem.isMIP()); + } + + @Test + void updateRelaxAndQuadraticInspectionMatchProblemContracts() { + Problem problem = new Problem("problem"); + Variable x = problem.addVariable(0.0, 5.0, 1.0, VariableType.INTEGER, "x"); + Variable y = problem.addVariable(0.0, 5.0, 2.0, VariableType.CONTINUOUS, "y"); + Constraint constraint = + problem.addConstraint(LinearExpression.of(x, 2.0).plus(y).le(7.0), "c"); + + problem.updateConstraint(constraint, Map.of(x, 1.0), 8.0); + assertEquals(1.0, constraint.getCoefficient(x)); + assertEquals(8.0, constraint.getRHS()); + + problem.updateObjective(Map.of(x, 3.0, y, 4.0), 5.0, ObjectiveSense.MAXIMIZE); + assertEquals(ObjectiveSense.MAXIMIZE, problem.getObjectiveSense()); + assertEquals(5.0, problem.getObjectiveConstant()); + assertEquals(3.0, x.getObjectiveCoefficient()); + assertEquals(4.0, y.getObjectiveCoefficient()); + + QuadraticExpression quadratic = QuadraticExpression.of(x, x, 2.0).plus(y, y, 3.0); + problem.setObjective(quadratic, ObjectiveSense.MINIMIZE); + assertTrue(problem.getObjective().isQuadratic()); + assertEquals(quadratic, problem.getObjective()); + CSRMatrix qcsr = problem.getQCSR(); + assertArrayEquals(new int[] {0, 1, 2}, qcsr.getRowOffsets()); + assertArrayEquals(new int[] {0, 1}, qcsr.getColumnIndices()); + assertArrayEquals(new double[] {2.0, 3.0}, qcsr.getValues()); + assertEquals(2, quadratic.getCoefficients().size()); + assertEquals(2.0, quadratic.getCoefficient(0)); + + Constraint quadraticConstraint = + problem.addConstraint(QuadraticExpression.of(x, x, 1.0).plus(y, y, 1.0).le(10.0), "qc"); + x.setValue(1.0); + y.setValue(2.0); + assertEquals(1, quadraticConstraint.getIndex()); + assertEquals(5.0, quadraticConstraint.computeSlack()); + assertEquals(1, problem.getQuadraticConstraints().size()); + + Problem relaxed = problem.relax(); + assertFalse(relaxed.isMIP()); + assertEquals(VariableType.CONTINUOUS, relaxed.getVariable(0).getVariableType()); + assertEquals("x", relaxed.getVariable(0).getVariableName()); + assertEquals("y", relaxed.getVariable(1).getVariableName()); + } +}