Dynamic Python bindings for the SolverForge constraint solver.
Project description
SolverForge Python
solverforge is the dynamic Python binding package for SolverForge. Python
users define models with Python classes, decorators, functions, and lambdas.
They do not write Rust and they do not pass JSON to a fixed backend.
The native extension owns the working solution state in Rust so SolverForge can clone, mutate, and snapshot solutions safely. Python callbacks are the single constraint authoring surface.
The package targets CPython 3.14 and Rust 1.95.0. This checkout consumes the
published SolverForge Rust crates 0.18.0 and pins solverforge-ui to 0.7.0
through exact crates.io versions in Cargo.toml and registry checksums in
Cargo.lock; it has no local path or Git dependency override. The solverforge
0.6.x line is the current dynamic binding architecture and intentionally
supersedes the older incompatible 0.2.x and 0.3.0 artifacts in the same PyPI
namespace. Those older artifacts exposed SolverFactory, PlanningVariable,
Java service requirements, and other APIs that are not part of this package.
Installation
Install the solverforge 0.6.1 release, built on the exact published and
locked SolverForge Rust 0.18.0 registry dependency base:
python3.14 -m pip install "solverforge==0.6.1"
The installable wheel contains the core solverforge package, native extension,
and embedded shared solverforge-ui assets exposed through
solverforge.ui.asset() and solverforge.ui.asset_paths().
Source-checkout examples, including the hospital and deliveries FastAPI apps and
their app-specific static files, are maintained in this repository rather than
installed into the runtime wheel.
Examples
Run source-current examples through the root Makefile:
make hospital-run
make deliveries-run PORT=7861
The same source-checkout examples can be run against the installed package
without PYTHONPATH or an editable checkout:
python3.14 -m venv .venv-examples
. .venv-examples/bin/activate
python -m pip install "solverforge[examples]==0.6.1"
python examples/nqueens.py
python -m examples.solverforge_hospital
python -m examples.solverforge_deliveries
Then open http://127.0.0.1:7860 for the hospital app or
http://127.0.0.1:7861 for the deliveries app.
For local package development, use the root Makefile targets instead.
Minimal Model
A minimal model uses Python decorators for domain metadata and plain Python callbacks for constraints:
from solverforge import (
ConstraintFactory,
HardSoftScore,
Solver,
constraint_provider,
planning_entity,
planning_solution,
planning_variable,
)
@planning_entity
class Shift:
nurse = planning_variable(value_range_provider="nurses", allows_unassigned=True)
def __init__(self, required: bool = True, nurse: int | None = None) -> None:
self.required = required
self.nurse = nurse
@constraint_provider
def constraints(factory: ConstraintFactory):
return [
factory.for_each(Shift)
.filter(lambda shift: shift.required and shift.nurse is None)
.penalize(HardSoftScore.ONE_HARD)
.named("required shift is unassigned")
]
@planning_solution(score=HardSoftScore, constraints=constraints)
class Schedule:
shifts: list[Shift]
def __init__(self, shifts: list[Shift], nurses: list[int]) -> None:
self.shifts = shifts
self.nurses = nurses
self.score = None
solution = Solver.solve(Schedule([Shift(), Shift()], [0, 1]))
Development
Local development is driven by the root Makefile, which creates .venv,
installs maturin and developer tools, and builds the PyO3 extension against the
current checkout.
make develop # release native extension installed into .venv
make install-playwright-system-deps # Linux Chromium browser libraries for CI
make test # Rust tests with Python link setup, then pytest
make lint # rustfmt check, ruff, mypy, and clippy
make ci-local # local CI simulation
make pre-release # ci-local plus release sdist/wheel checks
Run make help for focused targets such as make test-hospital,
make test-deliveries, make test-examples-browser, make docs-check,
make release-base-check, make test-one TEST=pattern, make hospital-run,
make hospital-solve, make deliveries-run, and make deliveries-solve.
Boundaries
- No generated Rust.
- No expression-object DSL.
- No string-parsed constraints.
- No duplicate 100% Python solver.
- No private upstream SolverForge modules; bindings use the public dynamic bridge contract.
- No fixed pre-modeled JSON-only backends.
- Public bridge work belongs upstream; the wrapper consumes only public seams rather than private upstream modules or a wrapper-owned solver path.
Current Support
-
Solver.solve(..., config=None)andSolverManager(config=None)load a user-spacesolver.tomlfrom the current directory when present. ExplicitSolverConfigordictconfigs are normalized before Rust handoff, and top-level plus phase-level termination fields match upstream SolverForge:seconds_spent_limit,minutes_spent_limit,best_score_limit,step_count_limit,unimproved_step_count_limit, andunimproved_seconds_spent_limit. -
Synchronous and retained scalar/list construction solves use one upstream compiled runtime runner. Ordinary dynamic scalar construction supports
first_fitandcheapest_insertion. A declared assignment group additionally supportsfirst_fit_decreasing,weakest_fit,weakest_fit_decreasing,strongest_fit, andstrongest_fit_decreasing; the decreasing variants requireentity_order, while weakest/strongest variants requirevalue_order. Dynamic list construction supportslist_round_robin,list_cheapest_insertion,list_regret_insertion,list_clarke_wright, andlist_k_opt, with the final two requiring their respective savings and route metadata bundles. These are core graph nodes over the immutable runtime model. An explicitgroup_namephase obeys its configured obligation and termination; required-only completion belongs solely to upstream omitted defaults. Default local search is assembled only when the top-level termination has an effective finite limit, so an empty or invalid termination cannot accidentally create an unbounded solve. There is no wrapper assignment placer, required stream, phase tree, TLS slot state, synthetic metric, or fallback path. -
SolverManageris backed by upstream retained jobs, statuses, events, and snapshots.SolverManager.solve(...)returnsJobHandle(job_id=...); the manager supports pause, resume, cancel, delete, and exact snapshot reads. Retained event payloads read current score from the attached solution snapshot when present. Long-running phases publish progress at the shared upstream cadence, and telemetry includes the current phase type, index, counters, and generation/evaluation time.telemetry_detail(job_id)returns one atomicstatuswith detailed telemetry plus an optionalcandidate_trace; ordinary status/event polling remains compact. -
Candidate tracing is an opt-in retained-job diagnostic configured as
candidate_trace = { max_entries = N }, whereNmust be non-zero. The core records the total pull count after the bounded identity prefix is full and marks truncation explicitly. SynchronousSolver.solve(...)rejectscandidate_tracebecause onlySolverManager.telemetry_detail(...)can return it. Trace format 3 exposes canonical configured input, execution policy, resolved phase-plan provenance, candidate identities and dispositions, prefix digests, completeness flags, and the explicitcandidate_index_scope = "source_local_only"contract.QualifiedCandidateTraceProvenance(...)accepts keyword-only lowercase SHA-256 values for the schema, instance, initial state, core tree, and loaded build plus a non-blank external producer. Pass it per job asSolverManager.solve(..., qualified_candidate_trace_provenance=...); it is rejected before schema discovery unless that manager has candidate tracing enabled. The provenance is never inferred from a solution or accepted through serializable solver config, and ordinary traces remain explicitlynot_requestedrather than being upgraded by the presence of digest fields. -
@candidate_metric("name")registers a named numeric move-ranking callback through@planning_solution(..., candidate_metrics=[...]). A metric receives a read-only solution callback view and the core candidate's canonical logical identity. Leaf selectors withselection_order = "sorted"or"probabilistic"must name a registeredselection_metric; other selection orders reject that field. Sorted metrics are ascending. Metric values must be finite, and probabilistic weights must also be non-negative, with zero-weight candidates omitted. -
planning_variable(...)supports row candidate callbacks and nearby value or entity candidate/distance metadata for dynamic scalar construction and nearby scalar local search. Each nearby metadata parameter has exactly one source: either a Python callback or a row field name. Dual raw-schema sources are rejected at compilation, while immutable row metadata can be supplied without per-query Python callbacks. Provider-backed value ranges are imported once per variable and shared across rows in Rust-owned state; row candidate callbacks remain row-specific. -
scalar_assignment_group(...)declares assignment-aware scalar groups for grouped scalar local search and assignment-group construction. Group metadata covers required entities, capacity keys, assignment rules, ordering callbacks, callback synchronization policy, andScalarGroupLimits. Assignment callbacks receive a solution view whose entity/fact collections reflect Rust-owned planning state and whose ordinary solution-level attributes remain available for read-only lookup context such as capacity tables. For invariant row metadata,required_entity_field,capacity_key_field,position_key_field, andsequence_key_fieldare explicit alternatives to their callbacks: required is a row bool, position and sequence are row integers, and capacity is a row list indexed by candidate value. A field alternative is mutually exclusive with its callback and avoids per-candidate Python transitions without caching or changing callback behavior. The compiled schema retains one immutable runtime plan (schema, descriptor, and model), reused by direct solves, manager jobs, snapshots, and resumes; instance rows, callback views, seeds, and moves remain per solve. An assignment-owned planning variable is exclusively edited through its declared grouped construction andgrouped_scalar_move_selector; a raw scalar, nearby, ruin, or conflict-repair selector targeting that same variable is rejected instead of creating a second search path. Generic@scalar_groupand conflict-repair callbacks are rejected in a local-search phase that could reach an assignment-owned slot: their returned edits are unscoped, so accepting them would recreate a second ownership path. Multiple declarative assignment groups can still compose in selector combinators. -
planning_list_variable(...)supportselement_owner,construction_element_order_key,precedence_duration, andprecedence_successorsfor owner-aware list moves and JSSP-style precedence/makespan scoring. Its route-related metadata has one canonical, nested declaration form:route=ListRouteHooks(...),savings=ListSavingsHooks(...), plus independentcross_position_distance=andintra_position_distance=sources. Route and savings are complete, independent bundles: a route bundle never implicitly enables savings construction. The owner, construction-order, duration, and successor metadata parameters each accept either a Python callback or the name of a solution-level sequence indexed by element ID; string sources let the native runtime read immutable element metadata without per-element Python calls. A declared sequence is mandatory before state import: owner entries areNoneor non-negative integers, order entries are integers, durations are non-negative integers, and successor entries are ordered sequences of non-negative integers. Missing or malformed sequences fail import; they never become an unrestricted owner, default order, or missing precedence edge. Nested route sources use explicit source wrappers rather than unscoped strings or callbacks. In a nested list bundle,RowField("..."),SolutionField("..."),EntityCallback(...), andSolutionCallback(...)make the source scope explicit.RowFieldreads only the declared entity row;SolutionFieldreads only the declared immutable solution-root value. Neither falls back to the other scope. Capacity feasibility is an explicitCapacityRouteFeasibility(capacity=..., demand=...)source whose two fields are independently scoped. A new nested declaration therefore never infers callback scope from arity or shares a route source with savings.Nearby list neighborhoods require their own
cross_position_distanceandintra_position_distancesources, and neither is inferred from a route distance. A matrix source for either metric is indexed by the actual list values at the two positions, never by their row or position indexes.shadow_variable_updates(...)registers post-update listeners whose native-owned fields are refreshed during solve/analyze and exported back to Python objects. Attached callback solution views synchronize dirty rows after the first full view sync instead of rewriting every entity row for repeated full-solution callbacks. -
Supported score families are
SoftScore,HardSoftScore,HardSoftDecimalScore, andHardMediumSoftScore. -
Python callback constraints are evaluated from Rust-owned dynamic state. Supported stream shapes are unary
for_each(...).filter(...), binaryfor_each(...).join(...).filter(...), and grouped-countfor_each(...).group_by(...), plusfor_each(...).balance(...),for_each_unassigned_element(owner_entity_type, variable_name), andlist_precedence_makespan(owner_entity_type, variable_name). Ordinary penalize/reward streams support fixed or callback-computed score weights; list precedence/makespan scoring is computed natively from owner, duration, and successor metadata callbacks or field-name metadata.indexed_presence(...)is available as a grouped collector for run/range presence scoring.joiner.equal(...)andjoiner.equal_bi(...)accept either Python key callbacks or attribute-name strings. Callback keys preserve Python equality semantics. String keys specialize to native equality only for planning scalar slots and stable fields stored directly on every imported row; properties, shadow-derived attributes, containers, and unsupported scalar values use live Python attribute lookup and equality. Schema/runtime plans reuse only capture-free functions with canonical module namespace provenance and no defaults or function-owned metadata. A callback from a different module namespace never shares a plan merely because its code object matches; mutable values in the same callback namespace remain live. Closures, bound defaults, partials, methods, callable instances, and other stateful callbacks compile per invocation rather than being retained across solves. Constraint plans specialize after state import; simple fixed-weight unary, list-unassigned, and proven string-key equality joins can evaluate directly from Rust-owned state while callback filters, callback weights, computed attributes, and unsupported key values use the Python callback surface. -
Dynamic scalar local search is available through upstream-style dynamic
change_move_selector,swap_move_selector,nearby_change_move_selector,nearby_swap_move_selector,pillar_change_move_selector,pillar_swap_move_selector,ruin_recreate_move_selector,grouped_scalar_move_selector,conflict_repair_move_selector, andcompound_conflict_repair_move_selectorphases. Raw scalar selectors exclude variables owned byscalar_assignment_group(...); use that group'sgrouped_scalar_move_selectorfor its assignment variable. -
Dynamic list local search is available through upstream-style
list_change_move_selector,nearby_list_change_move_selector,list_swap_move_selector,nearby_list_swap_move_selector,sublist_change_move_selector,sublist_swap_move_selector,list_reverse_move_selector,list_permute_move_selector,list_precedence_move_selector,k_opt_move_selector, andlist_ruin_move_selectorphases. -
limited_neighborhood,union_move_selector, and two-childcartesian_product_move_selectorcompose supported dynamic scalar and list selectors. Dynamic neighborhoods are opened as resumable cursor trees: union children, limits, Cartesian branches, pillar windows, and k-opt reconnections advance only when the solver requests the next candidate. Nearby candidate callbacks may return any Python iterable; candidates are consumed once into an exact bounded top-k rather than copied into a full ranked neighborhood. Stable candidate IDs remain borrowable for evaluation, losing candidates are released as soon as the forager no longer needs them, and only the selected move crosses the ownership boundary. Exact sorting, shuffling, and probability decorators retain the compact global ordering data their documented semantics require. -
examples/solverforge_hospitalis a 1:1 Python model of the Rust hospital use case's public dataset/config surface:LARGEhas 50 employees, 688 initially unassigned shifts, skill/availability-aware construction candidates, retained jobs, snapshots, analysis, pause/resume/cancel, and a reproducible 30-second hard-feasible terminal solve when installed with the release native extension. -
examples/solverforge_deliveriesis the Python port of the deliveries use case: initially unassigned delivery facts, route-owning vehicles, independent row-backedListRouteHooksandListSavingsHooksbundles with solution-scoped feasibility, route shadow metrics, unassigned-delivery scoring, retained jobs, route snapshots, analysis, and delivery-insertion recommendations. -
Top-level
ConstraintFactory.join,group_by,if_exists,if_not_exists, andflattenedremain explicit unsupported methods until the native callback stream planner has public bridge support for those top-level semantics. Use stream-levelfor_each(...).join(...)andfor_each(...).group_by(...)for the supported join and grouped-count surfaces.
Rust macro-generated SolverForge models remain the performance ceiling. The Python path preserves the Rust solver engine and Rust-owned state while paying the honest dynamic callback boundary cost.
Callback And Threading Contract
Python callbacks are the only constraint authoring surface. Filter callbacks
return bool; weight callbacks return a score object or integer; grouped scalar
callbacks return scalar edit candidates for @scalar_group; conflict repair
callbacks return scalar edit candidates for constraints declared with
@conflict_repair; candidate-metric callbacks return finite numeric ranking
values; value range providers return ordered finite collections; nearby
candidate callbacks return Python iterables; distance callbacks return numeric
distances; and owner hooks used by list selectors return an owner index or
None. Candidate metrics receive the callback solution view plus an operation
or composite identity dictionary; they rank an already generated core move and
do not rebuild or execute a second selector.
Callback exceptions are surfaced as SolverForge Python exceptions with the original Python traceback. Callback solution views include non-private, non-callable solution-level attributes from the imported Python solution, such as lookup tables and value sets used by hooks. Entity and fact collections in those views are projected from Rust-owned solver state so preview and best-solution clones never share mutable Python row objects with the working solution.
The primary target is CPython 3.14 free-threaded. The native extension declares that it does not require the GIL; Rust worker threads attach to Python only when invoking Python callbacks. Callback code must be deterministic for a given solution state, safe to run concurrently, and treat solution-level lookup context as immutable for the duration of a solve. Third-party Python extension modules used inside callbacks may still impose their own synchronization constraints.
License
SolverForge Python is licensed under the Apache License, Version 2.0. See
LICENSE for the full license text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file solverforge-0.6.1.tar.gz.
File metadata
- Download URL: solverforge-0.6.1.tar.gz
- Upload date:
- Size: 116.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12769997a54c155e2120951c9ac5ccda02dc73d93c992026e48baca90a37f2bf
|
|
| MD5 |
2c935a9be9177976ea842ebc68d78267
|
|
| BLAKE2b-256 |
fd01c4ca5d36b70aae8cb067163b48c7666014cef6a69bd254410bf51ea61a62
|
File details
Details for the file solverforge-0.6.1-cp314-cp314-win_amd64.whl.
File metadata
- Download URL: solverforge-0.6.1-cp314-cp314-win_amd64.whl
- Upload date:
- Size: 2.7 MB
- Tags: CPython 3.14, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
506c2ff8d3cfd6483e4bb4b11f73c3a2f170e5f72f0d7a88f9dc27ab03fde3a1
|
|
| MD5 |
893f8c9eddcb93a8207a902d4aa71886
|
|
| BLAKE2b-256 |
1c1bfed8cad162c35f538a0a5acadf3f81e84cec3a0d746e89b5c2b8e16ca353
|
File details
Details for the file solverforge-0.6.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: solverforge-0.6.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.14, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b657197e4f7f9a79d76d69d0333ff990ec2eda4ef7f1ce3ba009c9199b1d55e0
|
|
| MD5 |
d767aa85ecab53e1bcd703dd7db82203
|
|
| BLAKE2b-256 |
270820987567c96467d06079ce98adfc39767c67a2a2a775872b45414ec354ab
|
File details
Details for the file solverforge-0.6.1-cp314-cp314-macosx_11_0_arm64.whl.
File metadata
- Download URL: solverforge-0.6.1-cp314-cp314-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.6 MB
- Tags: CPython 3.14, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6d4061475bc7b9469ae3372abeba1af7eebf2502aa9b43eb440fcdb681b1607
|
|
| MD5 |
cc7bef9b16ce8a14db7d11810f3854dc
|
|
| BLAKE2b-256 |
04452fce2723b6a2a388b79c90e56ec2f94ba1e32f6fade70549f7d4152625f7
|