google / google/or-tools

Java: MPSolver.solve() throws IllegalArgumentException for status 99 when HiGHS stops at a limit

Open
#5,372 0 comments 0 reactions 0 assignees View on GitHub
Bug Lang: Java
Dominant language
C++
Stars
14.1k
Forks
2.5k
Avg merge
8h 39m
Merged PRs (30d)
72

Description

**What version of OR-Tools and what language are you using?**

Version: v9.15.6755 (latest release on Maven Central); the analysis below also refers to `main`
Language: Java (JDK 21)

**Which solver are you using (e.g. CP-SAT, Routing Solver, GLOP, BOP, Gurobi)**

HIGHS (HiGHS 1.12.0 as bundled in 9.15.6755), through both `MPSolver` (`ortools/linear_solver`) and `ModelBuilder`/`ModelSolver`.

**What operating system (Linux, Windows, ...) and version?**

Ubuntu Linux 24.04, kernel 6.17, x86-64, OpenJDK 21.0.12

**What did you do?**

Steps to reproduce the behavior:

1. Unpack the attached directory and run `./run.sh` (needs a JDK and maven; maven is only used to fetch `ortools-java` and its native jar). It compiles and runs the single file `HighsTimeLimitStatus.java`.
2. The reproducer builds a market split instance (Cornuéjols/Dawande): 4 equality rows, 26 binary variables plus 4 continuous slacks, minimizing the sum of the slacks. HiGHS needs about 10 seconds to prove optimality, so a 3 second limit is always hit while an incumbent is already known.
3. The same model is solved three times: with `MPSolver` and a 3 s time limit, with `ModelSolver` and a 3 s time limit, and with `ModelSolver` without a time limit.

**What did you expect to see**

HiGHS reports everything that is needed for a proper result. Its own log inside the OR-Tools run says:

```
Status Time limit reached
Primal bound 6
Dual bound 0
Solution status feasible
6 (objective)
```

So the expectation is:

* `MPSolver.solve()` returns `FEASIBLE` (or at the very least a valid `ResultStatus` value) and the incumbent is available through `MPVariable.solutionValue()`,
* `ModelSolver.solve()` returns `FEASIBLE` with `hasSolution() == true`.

**What did you see instead?**

1. `MPSolver.solve()` throws:

```
java.lang.IllegalArgumentException: No enum class com.google.ortools.linearsolver.MPSolver$ResultStatus with value 99
at com.google.ortools.linearsolver.MPSolver$ResultStatus.swigToEnum(MPSolver.java:760)
at com.google.ortools.linearsolver.MPSolver.solve(MPSolver.java:326)
```

2. `ModelSolver.solve()` returns `UNKNOWN_STATUS` with `hasSolution() == false`, i.e. the incumbent HiGHS found is silently dropped.

3. Without a time limit the very same model solves fine: `OPTIMAL`, objective 6.

Full output of the three runs: `output-ortools-9.15.6755.txt`.

**Analysis**

There problem consists of two parts:
1. the status/solution transfer from Highs to OrTools in C++, already fixed on main but no released yet,
2. an unchecked status cast instead of a specified status in the transfer from Ortools/C++ to Ortools/Java, not yet fixed on main.

Releasing part 1 would solve the specific problem, and fixing part 2 would add more stability in handling unknown status values.

Line numbers refer to the tags `v9.15` of or-tools and `v1.12.0`/`v1.15.1` of HiGHS; `main` line numbers are from 2026-09-09.

##### Part 1 - status mapping (already fixed on `main`, but in no release yet)

HiGHS returns `HighsStatus::kWarning` from `run()` whenever it stops at a limit, see `Highs::returnFromOptimizeModel` in `highs/lp_data/Highs.cpp` (lines 4672-4680 in HiGHS 1.12.0, lines 4863-4871 in 1.15.1 - the block is identical in both versions):

```cpp
4672 // Finally consider the warning returns
4673 case HighsModelStatus::kTimeLimit:
4674 case HighsModelStatus::kIterationLimit:
4675 case HighsModelStatus::kSolutionLimit:
4676 case HighsModelStatus::kInterrupt:
4677 case HighsModelStatus::kHighsInterrupt:
4678 case HighsModelStatus::kUnknown:
4679 assert(return_status == HighsStatus::kWarning);
4680 break;
```

In `ortools/linear_solver/proto_solver/highs_proto_solver.cc` of v9.15 that warning short-circuits the mapping, so the model status is never looked at (lines 225-236):

```cpp
225 HighsStatus run_status = highs.run();
226 switch (run_status) {
227 case HighsStatus::kError: {
...
232 case HighsStatus::kWarning: {
233 response.set_status_str("Warning HiGHS run()");
234 break; // model status is never inspected
235 }
236 case HighsStatus::kOk: {
```

The response therefore keeps the default of the proto field, which is 99 (`ortools/linear_solver/linear_solver.proto`, lines 605-606):

```proto
605 optional /*required*/ MPSolverResponseStatus status = 1
606 [default = MPSOLVER_UNKNOWN_STATUS];
```

On top of that, v9.15 copies the variable values only for `MPSOLVER_OPTIMAL` (`highs_proto_solver.cc:276`), so even a `MPSOLVER_FEASIBLE` response would arrive without a solution.

This part is [#5016 "Wrong status parsed for HiGHS when time_limit is set"](https://github.com/google/or-tools/issues/5016), fixed on `main` by commit `27ebae018` (2026-01-30): only `kError` returns early now, and the model status switch has explicit `kTimeLimit`/`kIterationLimit`/`kInterrupt`/`kSolutionLimit`/`kMemoryLimit` cases (`highs_proto_solver.cc:258-303` on current `main`). Commit `5ab516b43` (2026-02-03, "HiGHS: populate variable values when status is FEASIBLE") added the missing solution transfer (`highs_proto_solver.cc:324-325`). The `v9.15` tag is from 2026-01-09 and thus predates both, and 9.15.6755 is still the newest artifact on Maven Central, so there is currently no released version that contains the fix. A release containing those two commits would be very welcome.

##### Part 2 - the invalid enum value reaching Java (still present on `main`)

Independently of #5016, `MPSOLVER_UNKNOWN_STATUS` (99) is cast into an enum that does not have that value, in `ortools/linear_solver/linear_solver.cc` (lines 1151-1153, the same in v9.15 and on current `main`):

```cpp
1151 absl::Status MPSolver::LoadSolutionFromProto(const MPSolutionResponse& response,
1152 double tolerance) {
1153 interface_->result_status_ = static_cast(response.status());
```

`MPSolver::ResultStatus` (`ortools/linear_solver/linear_solver.h`) only has `OPTIMAL(0) … NOT_SOLVED(6)`, and the SWIG generated Java enum consequently throws in `MPSolver.java:760`. After #5016 this path is still reachable: the new code sets `MPSOLVER_UNKNOWN_STATUS` explicitly when a limit is reached *without* a feasible solution (`info.primal_solution_status != kSolutionStatusFeasible`), for instance when the time limit is so short that no incumbent has been found yet. A Java caller then gets an `IllegalArgumentException` out of `solve()` instead of a status it can handle.

Suggested fix: do not cast the response status unchecked. Either map every `MPSolverResponseStatus` without a counterpart to an existing value (`ABNORMAL` or `NOT_SOLVED`), or add the missing values (`MPSOLVER_UNKNOWN_STATUS`, `MPSOLVER_CANCELLED_BY_USER`) to `MPSolver::ResultStatus` so that SWIG can convert them.

**Anything else we should know about your project / environment**

* The problem is not specific to time limits: with the solver parameter `mip_max_improving_sols 1` (HiGHS stops with `kSolutionLimit`) the very same exception occurs.
* It is not a HiGHS problem. The standalone HiGHS CLI 1.15.1 reports the same thing as HiGHS 1.12.0 inside OR-Tools - "Time limit reached", "Solution status feasible", primal bound 6, see `output-highs-cli-1.15.1.txt` (`highs market-split.lp --time_limit 3`).
* Attached directory: [ortools-highs-status-bug.zip](https://github.com/user-attachments/files/32047693/ortools-highs-status-bug.zip)
* Files in this directory:
* market-split.lp - small MIP to reproduce the problem
* HighsTimeLimitStatus.java - small java program to reproduce the bug
* output-highs-cli-1.15.1.txt - output of standalone highs
* output-ortools-9.15.6755.txt - output of ortools/highs
* pom.xml - pom to download the ortools dependencies
* run.sh - run script

Contributor guide

Open the contributing guide

Research direction

Start with the Java reproducer HighsTimeLimitStatus.java and run.sh, then inspect LoadSolutionFromProto in ortools/linear_solver/linear_solver.cc and the ResultStatus definitions in linear_solver.h. Trace how MPSOLVER_UNKNOWN_STATUS reaches the Java binding and verify that unknown solver statuses no longer cause an IllegalArgumentException while preserving a usable result status.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, java
Domain
api, backend, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.