Documentation
Run synchronous solves, score analysis, compiled solver.toml config, retained jobs, candidate diagnostics, snapshots, and dynamic move selectors.
Python Solving & Runtime
SolverForge Python has two runtime entry points:
Solverfor synchronous solve and score-analysis callsSolverManagerfor retained jobs, lifecycle events, snapshots, pause, resume, cancel, and delete
Both paths compile the authored Python schema into one immutable SolverForge 0.19.1 runtime graph. The binding supplies dynamic state, callbacks, slot capabilities, assignment groups, providers, and candidate metrics; the core owns phase construction, cursor execution, foraging, lifecycle control, and telemetry. There is no wrapper-owned fallback runner.
Synchronous Solve
solved = Solver.solve(schedule)
Solver.solve(...) reads the Python model, runs the solver, then returns a
solution object with updated planning variables and score.
Pass config explicitly when the solve needs a seed, termination budget, or custom phase list:
solved = Solver.solve(
schedule,
config={
"random_seed": 7,
"termination": {"seconds_spent_limit": 5},
},
)
Score Analysis
analyzed = Solver.analyze(schedule)
print(analyzed.score)
Solver.analyze(...) evaluates callback constraints for the current solution
state and writes the calculated score back to the Python solution.
Config Loading
Config can be:
None- a
SolverConfig - a
dict[str, object] - a
solver.tomlfile in the current directory when the argument isNone
Supported termination fields are:
seconds_spent_limitminutes_spent_limitbest_score_limitstep_count_limitunimproved_step_count_limitunimproved_seconds_spent_limit
Termination fields can be set at the top level or under termination, but not
with conflicting values in both places.
random_seed = 7
[termination]
seconds_spent_limit = 5
[[phases]]
phase_type = "construction_heuristic"
[[phases]]
phase_type = "local_search"
move_selector = { type = "change_move_selector" }
Retained Jobs
Use SolverManager when the application needs lifecycle state, progress,
snapshots, or cancellation.
from solverforge import SolverManager
manager = SolverManager(config={"termination": {"seconds_spent_limit": 10}})
handle = manager.solve(schedule)
status = manager.wait(handle.job_id, timeout_seconds=15)
snapshot = manager.snapshot(handle.job_id)
print(status["lifecycle_state"])
print(snapshot.score)
The manager exposes:
| Method | Purpose |
|---|---|
solve(solution) |
start a retained job and return a JobHandle |
get_status(job_id) |
read lifecycle state and current job metadata |
telemetry_detail(job_id) |
atomically read detailed telemetry and an optional candidate trace |
events(job_id) |
drain retained lifecycle events |
wait(job_id, timeout_seconds=...) |
block until completed, cancelled, or failed |
snapshot(job_id, snapshot_revision=None) |
export a deep-copied Python solution snapshot |
pause(job_id) |
request pause |
resume(job_id) |
resume a paused job |
cancel(job_id) |
request cancellation |
delete(job_id) |
remove retained job state |
Treat snapshots as point-in-time Python objects. Status telemetry includes the active phase type, phase index, phase-local counters, and generation/evaluation time when a phase is active.
Mandatory Completion And Limits
Configured solver and phase limits remain binding during construction. A best or completed solution is published only after every planning-list element, required assignment row, and non-optional scalar variable is assigned.
If a limit is reached first, Solver.solve(...) raises a runtime error. A
retained job enters FAILED without a latest snapshot revision, and
snapshot(...) remains unavailable instead of returning an incomplete plan. A
pause requested before mandatory construction completes preserves resumable
internal state, but it does not expose a partial snapshot.
Candidate Tracing
Candidate tracing is opt-in, bounded, and available only for retained manager jobs:
manager = SolverManager(
{
"termination": {"seconds_spent_limit": 10},
"candidate_trace": {"max_entries": 256},
}
)
handle = manager.solve(schedule)
manager.wait(handle.job_id, timeout_seconds=15)
detail = manager.telemetry_detail(handle.job_id)
trace = detail["candidate_trace"]
max_entries must be greater than zero. The format-3 trace reports canonical
configured input, execution policy, resolved phase-plan provenance, candidate
identities and dispositions, prefix digests, explicit completeness state, and
truncation. The total pull count continues after the bounded identity prefix is
full.
Ordinary get_status(...), events(...), and snapshot(...) payloads do not
clone the trace. Solver.solve(...) rejects candidate_trace because the
synchronous API has no retained detail channel.
Use QualifiedCandidateTraceProvenance when an external harness has already
attested the schema, instance, initial state, core tree, and loaded build:
from solverforge import QualifiedCandidateTraceProvenance
provenance = QualifiedCandidateTraceProvenance(
schema_sha256="01" * 32,
instance_sha256="02" * 32,
initial_state_sha256="03" * 32,
core_tree_sha256="04" * 32,
build_sha256="05" * 32,
producer="solverforge-bench",
)
handle = manager.solve(
schedule,
qualified_candidate_trace_provenance=provenance,
)
The digests are keyword-only lowercase SHA-256 strings and producer must be
non-blank. Qualified provenance is accepted per job only when the manager has
candidate tracing enabled. It is never inferred from a solution or accepted as
ordinary serializable solver config.
Dynamic Move Support
Construction phases available to Python dynamic models include ordinary scalar first-fit and cheapest insertion. Assignment groups additionally support first-fit decreasing, weakest-fit, weakest-fit decreasing, strongest-fit, and strongest-fit decreasing; decreasing variants require entity order and weakest/strongest variants require value order. Dynamic list construction supports list round robin, cheapest insertion, regret insertion, Clarke-Wright, and k-opt polish when the model supplies the metadata bundle each phase requires.
An explicit assignment group_name obeys its configured obligation and
termination. Omitted construction resolves required and optional stages in the
core, but mandatory work never bypasses a configured limit. Default local
search is assembled only when the top-level termination has an effective finite
limit, so an empty or invalid termination cannot create an unbounded solve.
Scalar selectors available to Python dynamic models:
change_move_selectorswap_move_selectornearby_change_move_selectornearby_swap_move_selectorpillar_change_move_selectorpillar_swap_move_selectorruin_recreate_move_selectorgrouped_scalar_move_selectorconflict_repair_move_selectorcompound_conflict_repair_move_selector
List selectors available to Python dynamic models:
list_change_move_selectornearby_list_change_move_selectorlist_swap_move_selectornearby_list_swap_move_selectorsublist_change_move_selectorsublist_swap_move_selectorlist_reverse_move_selectorlist_permute_move_selectorlist_precedence_move_selectork_opt_move_selectorlist_ruin_move_selector
Selector combinators available to Python dynamic models:
limited_neighborhoodunion_move_selector- two-child
cartesian_product_move_selector
Grouped scalar and conflict-repair selectors require Python callbacks declared
on the solution with @scalar_group(...) and @conflict_repair(...).
Assignment-aware grouped scalar construction and grouped local search consume
scalar_assignment_group(...) metadata from the solution.
Assignment-owned variables are excluded from raw scalar, nearby, ruin, and conflict-repair selectors. Their declared group is the only construction and local-search ownership path. Multiple declarative assignment groups can still compose through selector combinators.
Dynamic neighborhoods are resumable cursor trees. Union children, limits, Cartesian branches, pillar windows, and k-opt reconnections advance only when the solver requests another candidate. Nearby Python callbacks may return any iterable; the binding consumes it once into an exact bounded top-k. Losing candidates are released when the forager no longer needs them, and only the winner crosses the ownership boundary.
Candidate Ordering
Leaf selectors can use original, seeded random, shuffled, sorted, or
probabilistic order. Sorted and probabilistic leaves name a candidate metric
registered through @candidate_metric and
@planning_solution(candidate_metrics=[...]):
[[phases]]
type = "local_search"
[phases.move_selector]
type = "change_move_selector"
entity_class = "Shift"
variable_name = "employee_idx"
selection_order = "sorted"
selection_metric = "dispatch_cost"
Sorted metrics are ascending. Metric results must be finite; probabilistic weights must also be non-negative, and zero-weight candidates are omitted.
Embedded UI Assets
The wheel embeds shared solverforge-ui assets through the native bridge.
Python HTTP hosts can serve those files without copying the UI bundle into each
example:
from solverforge import ui
asset = ui.asset("sf.js")
for path in ui.asset_paths():
print(path)
The hospital and deliveries examples use this bridge for /sf/* assets and
keep only app-specific browser modules under their own static/ directories.
Callback Threading
Python callbacks must be deterministic for the same solution state. The native extension may invoke callbacks repeatedly during scoring and search. On free-threaded CPython 3.14, callback code and third-party extension modules used inside callbacks need to be safe for the concurrency they participate in.
The native working solution stores scalar, list, and candidate values by compiled descriptor index and shares immutable metadata across clones. Callback views project entity and fact collections from that Rust-owned state, so working, preview, and best-solution clones do not share mutable Python row objects. After the first full callback-view sync, only changed rows are synchronized.
Callback exceptions surface as SolverForge Python exceptions with Python traceback context.