Python getting started

The Python package exposes a compact public API:

  • solve() solves an OMMX instance.

  • SolveOptions configures detailed solver behavior.

Build and solve a model with JijModeling

Define a binary knapsack model with JijModeling, evaluate it with concrete data to create an OMMX instance, and pass that instance directly to solve:

import jijmodeling as jm

from jijzept_solver import solve

problem = jm.Problem("knapsack", sense=jm.ProblemSense.MAXIMIZE)

values = problem.Float("values", ndim=1)
weights = problem.Float("weights", ndim=1)
capacity = problem.Float("capacity")
selected = problem.BinaryVar("selected", shape=values.len_at(0))

problem += (values * selected).sum()
problem += problem.Constraint(
    "capacity",
    (weights * selected).sum() <= capacity,
)

instance = problem.eval(
    {
        "values": [10, 13, 18, 31],
        "weights": [11, 15, 20, 35],
        "capacity": 47,
    }
)

solution = solve(
    instance,
    time_limit=60.0,
    gap_limit=0.01,
    verbosity=1,
)

print(f"feasible: {solution.feasible}")
print(f"objective: {solution.objective}")

Problem.eval() evaluates the placeholders and produces the OMMX instance accepted by JijZept Solver. There is no separate JijModeling-to-OMMX conversion step.

Solve an MPS model

If a model is already available as MPS, load it as an OMMX instance and call solve in the same way:

from ommx.v1 import Instance

from jijzept_solver import solve

instance = Instance.load_mps("model.mps")
solution = solve(
    instance,
    time_limit=60.0,
    gap_limit=0.01,
    verbosity=1,
)

print(f"feasible: {solution.feasible}")
print(f"objective: {solution.objective}")

gap_limit=0.01 stops once the relative optimality gap reaches one percent. Omit time_limit, or pass it explicitly as None, to run without a time limit.

Configure detailed options

Use SolveOptions for settings that are not part of the convenience arguments on solve:

from jijzept_solver import SolveOptions, solve

options = SolveOptions(
    branching="reliability",
    node_selector="depth-first",
    enable_feasibility_pump=True,
    num_threads=4,
)

solution = solve(instance, options=options, time_limit=60.0)

Settings have the following precedence, from lowest to highest:

  1. Solver defaults

  2. SolveOptions

  3. Convenience arguments passed directly to solve

This makes it possible to reuse an options object while overriding a small number of values for one solve.

Save and load options

Options can be shared as a TOML file:

from jijzept_solver import SolveOptions

options = SolveOptions(time_limit=120.0, num_threads=8)
options.to_file("solver-options.toml")

loaded = SolveOptions.from_file("solver-options.toml")

To create a documented template containing every option:

SolveOptions.write_template("solver-options.toml")

Handle interruption

solve supports Ctrl+C. If an incumbent exists, the solver returns the best solution found so far and emits a warning. If no solution has been found, it raises KeyboardInterrupt.

See the Python API reference for the complete signatures and option descriptions.

Reusing an initial solution across sessions

Decision-variable IDs can change when a model is compiled again. To reuse values across compilations, pass names and subscript tuples to initial_state:

initial = {
    "x": {(0,): 1.0, (1,): 0.0},
    "scalar_variable": {(): 2.0},
}
solution = solve(current_instance, initial_state=initial)

For a variable with multiple indices, such as x[0, 1, 2, 3], use the entire index tuple. The following nested and flat dictionaries are equivalent:

# Nested form: variable name, then the four indices.
initial = {"x": {(0, 1, 2, 3): 1.0}}
solution = solve(current_instance, initial_state=initial)

# Flat form: (variable name, index tuple) as one key.
initial = {("x", (0, 1, 2, 3)): 1.0}
solution = solve(current_instance, initial_state=initial)

The equivalent flat form is {("x", (0,)): 1.0}. Use () for scalar variables and (i,) for one-dimensional subscripts. These are Python dictionaries, not a JSON wire format. Resolve names against the instance being solved, not a previous instance’s numeric IDs. Names and subscripts must still identify the same modeling variables in the new problem.

Partial dictionaries are allowed: omitted variables retain the solver’s existing default initialization. Every supplied name/subscript pair must resolve uniquely, but unrelated variables need not have names. Variables without names can still be initialized using their current numeric IDs or an existing ommx.v1.State. An empty subscript tuple is valid for a named scalar; it is not missing metadata. Unknown names/subscripts and ambiguous duplicate name/subscript pairs raise ValueError; malformed keys or nonnumeric values raise TypeError. Distinct Python keys that become identical after native name or integer conversion raise ValueError rather than overwriting values. Mapping values must be finite real numbers, not booleans. An infeasible start is allowed, and out-of-bounds values retain the solver’s existing clamping behavior. Always check solution.feasible.

Existing ommx.v1.State objects remain supported unchanged. Numeric ID dictionaries such as {0: 1.0} are also accepted, but only use these with IDs from the current instance. Extra numeric IDs retain the existing behavior of being ignored. Do not mix ID, flat named, and nested named keys within one dictionary. This convenience API does not change the Rust or serialized OMMX State format, nor does it guarantee that presolve behaves identically across platforms.