0

Ortools

Fresh

Train and evaluate LLMs on **designing solutions and algorithms for hard operations research problems**, with a verifiable reward: the objective of the agent's solution against an OR-Tools reference.

Type
RL Env
License
bsd-3-clause
Published
Nov 2026

Cite

Notes

Only stored in your browser.

OR-Tools Environment

Train and evaluate LLMs on designing solutions and algorithms for hard operations research problems, with a verifiable reward: the objective of the agent's solution against an OR-Tools reference.

The setup follows LLMs Can Design Near-Optimal OR Algorithms. Each episode is one query: the agent reads a problem statement, uses a Python sandbox under a compute budget, and returns one deliverable.

LevelThe agent getsIt submitsScored on
1one instancea solution (submit_solution)that instance
2the problem class and an example instancecode defining solve(instance) (submit_algorithm)unseen instances of the same distribution

Problems

The problem classes were picked where exact methods still struggle at scale, so a strong agent has room to match or beat the reference instead of saturating at the optimum. Each class has a generator that follows the standard benchmark design, a loader for the standard file format, a pure-Python verifier, and an OR-Tools reference solver taken from the OR-Tools examples.

problemProblemGeneratorSizesBenchmark filesReference
jobshopJob shop, minimize makespanTaillard (1993), reproduces ta01-ta80 from their seeds6x6 … 100x20OR-Library / JSPLIBCP-SAT (optimum and bound)
rcpspResource-constrained project schedulingPSPLIB design (resource factor, resource strength)j10 … j300PSPLIB .smCP-SAT (optimum and bound)
cvrpCapacitated vehicle routingUchoa et al. (2017) "X" designn20 … n1000CVRPLIB .vrpRouting solver, guided local search
cvrptwVehicle routing with time windowsSolomon (1987) / Gehring & Homberger classes R, C, RC, series 1 and 2n25 … n1000Solomon / Gehring-HombergerRouting solver, guided local search
tardinessSingle-machine total weighted tardinessOR-Library (Potts & Van Wassenhove)n10 … n500OR-Library wtN.txtCP-SAT (optimum and bound)
rosteringNurse rostering with hard and soft work rulesRules of shift_scheduling_sat.py, random demands and requests4x1 … 80x8 (employees x weeks)—CP-SAT (optimum and bound)
timetablingCurriculum-based course timetablingITC-2007 track 3 rules, generated around a hidden feasible timetablec10 … c150 (courses)ITC-2007 .cttCP-SAT (optimum and bound)

The smallest size of each problem is for smoke tests. Every generated instance is reproducible from (seed, size), and the generators randomize the structural parameters (resource strength, due-date tightness, depot and customer placement, demand distribution) per seed.

Reward

All objectives are minimized. The reward places the agent's objective between a naive solution and the OR-Tools reference:

reward = (naive - objective) / (naive - reference)

1.0 matches the reference and above 1.0 beats it; 0 is no better than the naive solution, and worse solutions score below 0 down to -1.0, which is also the score of an infeasible solution, a crash or a timeout. At level 2 the reward is the mean over the evaluation instances. The episode ends at the first submission, whose observation carries done=True and the reward.

The naive solution makes no attempt to optimize, so a policy cannot collect reward by submitting any feasible answer (a plain reference / objective ratio gives a random job order on 10 jobs about 0.5). Each problem describes its naive solution in BASELINE and computes it with baseline(instance):

problemNaive solution
jobshopoperations scheduled round by round in job order, each as early as possible
rcpspserial schedule in activity index order
cvrpcustomers in index order, a new route whenever the next one does not fit
cvrptwcustomers in index order, a new route whenever the next one breaks the capacity or a time window
tardinessjobs in index order
rosteringfirst feasible roster CP-SAT finds when all penalties are ignored
timetablingfirst feasible timetable CP-SAT finds when all penalties are ignored

The rostering and timetabling baselines come from a solver, so they can vary with solver time and threads. Precompute them with build_dataset for stable rewards.

References are best computed offline with a long time limit:

PYTHONPATH=src:envs python -m ortools_env.build_dataset --problem rcpsp --sizes j60 j120 --level 1 \
    --num-rows 256 --reference-time-limit 60 --out rcpsp_l1.jsonl

Each row holds the reset arguments of one episode (problem, size, level, instance_seed, reference and baseline, or eval_seeds, eval_references and eval_baselines). A reference missing from reset is computed at submission time with reference_time_limit_s, and so is a missing baseline.

Quick start

PYTHONPATH=src:envs uvicorn ortools_env.server.app:app --port 8000
from openenv.core.env_server.mcp_types import CallToolAction

from ortools_env import ORToolsEnv

with ORToolsEnv(base_url="http://localhost:8000").sync() as env:
    result = env.reset(problem="jobshop", size="15x15", level=1, seed=0)
    print(result.observation.metadata["prompt"])

    print(env.call_tool("run_python", code="print(instance['num_jobs'])"))

    # `call_tool` returns only the tool text; use `step` to read the reward of the submission.
    result = env.step(
        CallToolAction(
            tool_name="submit_solution", arguments={"solution": {"starts": [...]}}
        )
    )
    print(result.reward, result.done)
    print(env.state())  # ORToolsState: objective, reference, compute used, ...

examples/ortools_env_episode.py plays a full level-1 episode through the tools. To train on this environment with GRPO, see examples/grpo_ortools on a TRL branch.

Tools

ToolDescription
run_python(code)Runs a script in the sandbox with the instance bound to instance (and in instance.json); files persist between calls. Counts against the compute budget.
check_solution(solution)Level 1: returns the objective of a candidate solution and the naive objective, or why it is infeasible. Counts as a Python call.
test_algorithm(code)Level 2: runs solve on the example instance as at submission and returns its objective and the naive objective, or why it failed. Counts against the Python calls and the compute budget.
submit_solution(solution)Level 1: submits the solution as a JSON object. Ends the episode.
submit_algorithm(code)Level 2: submits code defining solve(instance), run in a fresh process per evaluation instance with solve_time_limit_s. Ends the episode.

Reset arguments

ArgumentDefaultDescription
problem"jobshop"jobshop, rcpsp, cvrp, cvrptw, tardiness, rostering or timetabling
sizethe problem's defaulta key of the problem's SIZES
level11 (solution) or 2 (algorithm)
instance / instance_seedgenerated from seedan explicit instance, e.g. read with the problem's load, or the seed of a generated one
reference, baseline, lower_boundcomputed at submissionprecomputed reference, naive objective and bound (level 1)
eval_seeds, eval_references, eval_baselinesnum_eval_instances seeds from seedhidden evaluation instances, their references and naive objectives (level 2)
allow_ortoolsFalsewhether the agent's code may import ortools
compute_budget_s, max_python_calls, python_timeout_s600, 50, 120sandbox budget, shared by run_python and the checks
solve_time_limit_s30per-instance time limit of solve (level 2)
reference_time_limit_s10OR-Tools time limit for references computed at submission

Sandbox

Code runs in a subprocess with a wall-clock limit and, on Linux, a 4 GB per-process address-space limit. Cleanup kills the original process group, but detached children can survive. This does not provide aggregate resource containment. Hostile submissions remain blocked on per-run Linux PID/cgroup isolation; see Sandbox status and deployment holds.

Keeping ortools (unless allow_ortools=True) and the environment's own modules out of reach prevents direct imports of the reference solver and verifier. This is only one part of the required isolation boundary.

In the container, the code runs as an unprivileged sandbox user with its own interpreter (numpy and scipy only), and the environment source and the server's packages are not readable by that user; with allow_ortools=True it uses a second interpreter with ortools. This is configured by SANDBOX_USER, SANDBOX_PYTHON and SANDBOX_ORTOOLS_PYTHON in server/Dockerfile. Outside the container (SANDBOX_USER unset), the code runs with the server's interpreter and an in-process import blocker that it can remove, which is fine for development but does not keep rewards honest against a policy that looks for it.

Deployment

This environment is excluded from automatic Spaces releases by SKIP_HF_DEPLOYMENT, because the sandbox is not ready for hostile submissions (see Sandbox status and deployment holds). Do not publish it as a public Space. Run it locally or on private infrastructure, with one container per session, no outbound network and container limits.

The isolation described under Sandbox only exists in the image:

docker build -t ortools-env -f envs/ortools_env/server/Dockerfile envs/ortools_env
docker network create --internal ortools-net
docker run --rm --name ortools-env --network ortools-net --pids-limit 256 --memory 8g ortools-env

An --internal network has no outbound access and published ports do not reach it, so run the client in a container on ortools-net and connect to http://ortools-env:8000. These limits apply to the whole container, not to each submission. Submitted code can still reach the server's own port from inside the container.

Harbor export

The same episodes can be exported as Harbor tasks to measure frontier agents (claude-code, codex, ...) with their own harnesses, for example through the harbor_env environment:

PYTHONPATH=src:envs python -m ortools_env.export_harbor --dataset rcpsp_l1.jsonl --out tasks/

Each task gets the instance in /app/instance.json and asks for /app/solution.json (level 1) or /app/solve.py (level 2). The verifier, the problem module and the instances it scores are in tests/, which Harbor only uploads after the agent has finished.

Nothing stops an agent from installing ortools in a Harbor task, so do not compare these scores directly with allow_ortools=False runs.