Using a solver
This guide walks the whole consumer side. If you just want something running, start with the Quickstart.
1. Open a library
Section titled “1. Open a library”Find one by name, or open a specific file:
import fznso
lib = fznso.Library.find("gecode") # newest installedlib = fznso.Library.find("gecode", "6") # newest 6.x.ylib = fznso.Library("/path/to/libgecode.so") # this exact fileuse fznso::Library;
let lib = unsafe { Library::find("gecode") }?;let lib = unsafe { Library::find_version("gecode", "6") }?;let lib = unsafe { Library::new("/path/to/libgecode.so") }?;Opening a library is unsafe because it runs the file’s initialisers: you are trusting the file, and no amount of checking afterwards changes that.
Everything past this point is safe.
#include "fznso.hpp"
auto lib = fznso::Library::find("gecode");auto lib = fznso::Library::find_version("gecode", "6");fznso::Library lib{"/path/to/libgecode.so"};The ABI version is checked here, before any other entry point is called; a mismatched solver fails to load rather than crashing later. If nothing is found, see Installing solvers.
2. Ask what it can do
Section titled “2. Ask what it can do”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.
The capability lists are not exposed to Python yet. Read them from Rust or C++, or drive the solver by names you already know.
lib.constraint_types(); // identifiers and argument typeslib.decision_types(); // decision-variable types it can createlib.objective_list(); // objective strategieslib.option_list(); // options, with types and defaultslib.statistic_list(); // statistics, and where each can be read fromlib->constraint_types(); // identifiers and argument typeslib->decision_types(); // decision-variable types it can createlib->objectives(); // objective strategieslib->options(); // options, with types and defaultslib->statistics(); // statistics, and where each can be read fromDo 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.
3. Create an instance
Section titled “3. Create an instance”solver = lib.create_solver()let mut solver = lib.create_solver();fznso::DynSolver solver = lib->create_solver();One instance holds one configuration and whatever search state survives between runs. Create several if you want to search several problems at once; instances do not share state.
4. Describe the problem
Section titled “4. Describe the problem”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..10y = model.add_decision(var_int, range(1, 11), name="y")model.add_constraint("int_lin_le", [[1, 1], [x, y], 12]) # x + y <= 12model.add_constraint("int_lin_ne", [[1, -1], [x, y], 0]) # x != ymodel.set_objective("int_maximize", x)use fznso::{LayeredModel, OwnedValue, RangeList, Type, TypeBase};
let var_int = Type::new(TypeBase::FznsoTypeBaseInt).decision(true);let mut model = LayeredModel::default();let x = model.add_decision( var_int, OwnedValue::IntSet(RangeList::from(1_i64..=10)), Some("x".into()), false, true, vec![],);let _ = model.add_constraint("int_all_different", vec![xs.clone()], None, vec![]);const fznso::Type INT_VAR = fznso::Type{FznsoTypeBaseInt}.decision(true);
fznso::LayeredModel model;fznso::Decision x = model.add_decision(INT_VAR, fznso::OwnedValue::int_range(1, 10), "x");model.add_constraint("int_all_different", {fznso::OwnedValue::list({fznso::OwnedValue{x}})});model.set_objective("int_maximize", fznso::OwnedValue{x});If your application already holds the problem in its own structures, implement the model interface over them instead of copying into a LayeredModel. That is what the callback design is for.
See The model.
5. Configure
Section titled “5. Configure”solver.option_set("time_limit", 30_000) # millisecondssolver.option_set("threads", 8)solver.option_set("intermediate", True) # report improving solutions as they are foundsolver.option_set("time_limit", (&30_000_i64).into())?; // millisecondssolver.option_set("threads", (&8_i64).into())?;solver.option_set("intermediate", (&true).into())?;solver.option_set("time_limit", fznso::OwnedValue{std::int64_t{30000}}); // millisecondssolver.option_set("threads", fznso::OwnedValue{std::int64_t{8}});solver.option_set("intermediate", fznso::OwnedValue{true});Setting an option the solver did not declare fails and carries a message. The common options are in the registry.
6. Run
Section titled “6. Run”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}"),)let mut best = None;let status = solver.run( &model, &mut |sol: &fznso::DynSolution<'_>| best = Some(sol.value(0).get_int()), Some(&mut |scope: &str, value: fznso::Value<'_>| eprintln!("[{scope}] {value:?}")), fznso::dont_interrupt(),);std::optional<std::int64_t> best;fznso::Status status = solver.run( model, [&](const fznso::Solution& sol) { best = sol[x].as_int(); }, [&](std::string_view scope, const fznso::Value& v) { std::cerr << '[' << scope << "] " << v.as_string() << '\n'; });7. Read the outcome
Section titled “7. Read the outcome”run returns one of three statuses: Complete (the search finished — with an objective, the last solution reported is optimal), Incomplete (it stopped early, so what you have is valid but not necessarily best), or Error.
Solver-scoped statistics stay readable after the run:
print(solver.statistic("nodes"), solver.statistic("solve_time"))println!("{}", solver.statistic("nodes").get_int());std::cout << solver.statistic("nodes").as_int() << '\n';8. Stopping early
Section titled “8. Stopping early”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 merely a special case of the second.
stop = threading.Event()solver.run(model, on_solution=cb, should_stop=stop.is_set)let stop = AtomicBool::new(false);solver.run( &model, &mut on_solution, fznso::ignore_messages(), Some(&|| stop.load(Ordering::Relaxed)),);std::atomic<bool> stop{false};solver.run(model, on_solution, [](std::string_view, const fznso::Value&) {}, [&] { return stop.load(std::memory_order_relaxed); });should_stop is the one callback you must make thread-safe.
Reading an atomic flag, as above, already is.
9. Run again
Section titled “9. Run again”Change the model and call run again on the same instance.
That is what the interface is for; see Incremental solving.
Complete working consumers
Section titled “Complete working consumers”The test suites drive a real solver end to end and are kept working by CI:
rust/fznso/tests/dylib.rscpp/tests/test_consumer.cpppython/tests/test_fznso.py