Skip to content

Using a solver

The whole consumer side. For something running in twenty lines, start with the Quickstart.

Find one by name, or open a specific file:

import fznso
lib = fznso.Library.find("gecode") # newest installed
lib = fznso.Library.find("gecode", "6") # newest 6.x.y
lib = fznso.Library("/path/to/libgecode.so") # this exact file

The ABI version is checked here, before any other entry point is called, so a mismatched solver fails to load rather than crashing later. If nothing is found, see Installing solvers.

A library declares its capabilities in five lists, fixed for its lifetime. Read them if you need to adapt; skip them if you already know the solver.

lib.constraint_types() # identifiers and argument types
lib.decision_types() # decision-variable types it can create
lib.objectives() # objective strategies
lib.options() # options, with types and defaults
lib.statistics() # statistics, and where each can be read from

Each call copies the library’s list into ordinary Python objects — opt.ident, opt.argument_type, opt.default — so read them once and keep what you need.

Do not pass a name that is not in these lists, and check the argument types too — a solver may accept a constraint only in a narrower form than the registry declares.

The simplest route is LayeredModel, an in-memory model the bindings provide:

var_int = fznso.Type("int", decision=True)
model = fznso.LayeredModel()
x = model.add_decision(var_int, range(1, 11), name="x") # x in 1..10
y = model.add_decision(var_int, range(1, 11), name="y")
model.add_constraint("int_lin_le", [[1, 1], [x, y], 12]) # x + y <= 12
model.add_constraint("int_lin_ne", [[1, -1], [x, y], 0]) # x != y
model.set_objective("int_maximize", x)

If your application already holds the problem in its own structures, implement the model interface over them rather than copying into a LayeredModel. That is what the callback design is for.

One instance holds one configuration and whatever search state survives between runs. Create several to search several problems at once; instances share nothing.

solver = lib.create_solver()
solver.option_set("time_limit", 30_000) # milliseconds
solver.option_set("threads", 8)
solver.option_set("intermediate", True) # report improving solutions as they are found
best = None
def on_solution(sol):
global best
best = (sol[x], sol[y]) # copied out — see the warning below
print("found", best, "nodes:", sol.statistic("nodes"))
status = solver.run(
model,
on_solution=on_solution,
on_message=lambda scope, value: print(f"[{scope}] {value}"),
)

Setting an option the solver did not declare fails and carries a message; the common options are in the registry.

A run that finished reports one of two statuses: Complete, meaning the search space was exhausted — so with an objective, the last solution reported is optimal — or Incomplete, meaning it stopped early and what you have is valid but not necessarily best.

A solver that could not continue reports a failure instead, carrying its own message. The bindings call solver_read_error for you, so that message arrives with the failure:

try:
status = solver.run(model, on_solution=on_solution)
except RuntimeError as err:
print("solver failed:", err) # the solver's own message
else:
print(status) # Status.Complete or Status.Incomplete

option_set fails the same way, and for the same reason: a message rather than a status — Result<(), String> in Rust, std::optional<std::string> in C++, a raised RuntimeError in Python.

Solver-scoped statistics stay readable after the run:

print(solver.statistic("nodes"), solver.statistic("solve_time"))

For a deadline set time_limit; for anything a deadline cannot express — a cancel button, “five solutions is enough” — pass a should_stop predicate. Stopping early covers why the first is not a special case of the second.

stop = threading.Event()
solver.run(model, on_solution=cb, should_stop=stop.is_set)

should_stop is the one callback you must make thread-safe; reading an atomic flag, as above, already is.

Change the model and call run again on the same instance — see Incremental solving.

Complete consumers, kept working by CI: rust/fznso/tests/dylib.rs, cpp/tests/test_consumer.cpp, python/tests/test_fznso.py.