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 pins upstream
SolverForge Rust crates to 0.17.2 and solverforge-ui to 0.7.0 through
Cargo.toml and Cargo.lock. The solverforge 0.5.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
This checkout builds solverforge 0.5.0. As of July 4, 2026, the v0.5.0
tagged release artifacts have passed GitHub release verification and are waiting
at the reviewed PyPI publish environment; PyPI and TestPyPI still resolve
solverforge to 0.4.0. Use the root Makefile targets for source-current
validation until that publish approval completes.
After the release approval completes:
python3.14 -m pip install "solverforge==0.5.0"
The installable wheel contains the core solverforge package, native extension,
and embedded shared solverforge-ui assets exposed through solverforge.ui.
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 while the 0.5.0 PyPI
publish job is awaiting approval:
make hospital-run
make deliveries-run PORT=7861
After 0.5.0 is published to PyPI, 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.5.0"
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.
- No upstream SolverForge mutation in this repository; upstream bridge work must land upstream first and then be consumed through released crates.
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 upstream
SolverForge. Dynamic scalar construction binds first-fit, cheapest insertion,
and assignment-group first-fit or cheapest insertion phases when the
construction phase declares
group_name; dynamic list construction binds list cheapest insertion, list regret insertion, list Clarke-Wright, and list k-opt phases where the model supplies the required list or route hooks. SolverManageris backed by upstream retained jobs, statuses, events, and snapshots.SolverManager.solve(...)returnsJobHandle(job_id=...); the manager supports pause, resume, cancel, delete, exact snapshot reads, and retained event payloads whose current score is read from the attached solution snapshot when present.planning_variable(...)supports row candidate callbacks and nearby value or entity candidate/distance callbacks for dynamic scalar construction and nearby scalar local search.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.planning_list_variable(...)supportselement_owner,construction_element_order_key,precedence_duration,precedence_successors, route callbacks (route_depot,route_metric_class,route_distance,route_feasible), and entity-scoped route callbacks (route_depot_entity,route_metric_class_entity,route_distance_entity,route_feasible_entity) for owner-aware list moves, JSSP-style precedence/makespan scoring, and CVRP-style route construction. CVRP-style models with immutable row data can avoid per-query Python callbacks by passingroute_depot_field,route_metric_class_field,route_distance_matrix_field,route_capacity_field, androute_demand_field; the Rust route phases read those fields through the active cursor and keep the callback options as fallbacks.shadow_variable_updates(...)registers post-update listeners whose native-owned fields are refreshed during solve/analyze and exported back to Python objects.- 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 callbacks.indexed_presence(...)is available as a grouped collector for run/range presence scoring.joiner.equal(...)andjoiner.equal_bi(...)preserve Python equality semantics. - 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. - 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.examples/solverforge_hospitalis a 1:1 Python model of the Rust hospital use case's public dataset/config surface:LARGEhas 50 employees, 688 shifts, retained jobs, snapshots, analysis, pause/resume/cancel, and a 30-second hard-feasible terminal solve when installed with the release native extension.examples/solverforge_deliveriesis the Python port of the deliveries use case: route-owning vehicles, delivery facts, CVRP route callbacks, 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; value range providers return ordered finite collections;
distance callbacks return numeric distances; and owner hooks used by list
selectors return an owner index or None.
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.
Release State
This repository publishes the solverforge package on PyPI. The current
checkout is 0.5.0, carrying the SolverForge Rust dependency base forward to
0.17.2. The v0.5.0 tag points at the source-current release head, GitHub CI
and Forgejo CI are green for that head, and the GitHub release workflow has built
and verified the source distribution plus Linux, macOS, and Windows wheels. As
of July 4, 2026, the release workflow is waiting at the reviewed pypi
environment before publishing.
PyPI latest for solverforge is still 0.4.0, with published versions 0.2.2,
0.2.3, 0.2.4, 0.2.5, 0.2.6, 0.3.0, and 0.4.0; TestPyPI also has
0.4.0. The old PyPI artifacts describe a different API and architecture,
including SolverFactory, PlanningVariable, and Java-service requirements.
Release rules:
- Publish the verified final
0.5.0, not a prerelease. - Keep
requires-python = ">=3.14". - Build wheels and the source distribution from this repository, with the
SolverForge Rust dependency base declared in
Cargo.tomland locked inCargo.lock. - Publish to TestPyPI only from
workflow_dispatchthrough trusted publishing. Taggedv*.*.*pushes build and verify release artifacts, then publish to PyPI only after the reviewedpypienvironment approves the job. - After PyPI publication, verify
python3.14 -m pip install solverforgeresolves to0.5.0. - After
0.5.0is published and smoke-tested, yank PyPI0.2.2through0.3.0with the reasonSuperseded by solverforge 0.5.0 dynamic Python binding architecture.
Trusted publishing should trust owner SolverForge, repository
solverforge-py, workflow release.yml, TestPyPI environment testpypi, and
PyPI environment pypi. The pypi GitHub environment should require manual
approval, and the workflow uses OIDC rather than long-lived PyPI tokens. Run
make pre-release before publishing; it checks the SolverForge dependency base,
runs local CI, builds release artifacts, runs twine check, and verifies
artifact contents.
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.5.0.tar.gz.
File metadata
- Download URL: solverforge-0.5.0.tar.gz
- Upload date:
- Size: 217.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 |
f3665cd0d3094e9288f9ac6a0306f9d4dffa860764a2d1a46384efbbf247d54a
|
|
| MD5 |
8f18f771a18e7d1df4a5a4835273da98
|
|
| BLAKE2b-256 |
0eec6cb59b9e73baa9e164900fe61ef39b6f336ca1d1b5409f98ab8e9ec21f4a
|
File details
Details for the file solverforge-0.5.0-cp314-cp314-win_amd64.whl.
File metadata
- Download URL: solverforge-0.5.0-cp314-cp314-win_amd64.whl
- Upload date:
- Size: 2.4 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 |
bb22596222a0f623b36388de7d6c8193764179d8c03f019c35b2973a38f8b7ab
|
|
| MD5 |
125884584a2bb0ef8d246c67deeccb8d
|
|
| BLAKE2b-256 |
405ad7d37c6fdc283649bc2d5bf221960427d915d5f48f4896e87671c09afee4
|
File details
Details for the file solverforge-0.5.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: solverforge-0.5.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 2.5 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 |
d4e842920e2bc7081ca3e356c5b19201be5ba892d6df890235a49d31de53498a
|
|
| MD5 |
a02c9ee69bbafae5207ee8e7a74f7c0d
|
|
| BLAKE2b-256 |
3385ced1975b5dd85abbc474e3cd6416cd6bd520cc3573e58080c2a5fa37dc34
|
File details
Details for the file solverforge-0.5.0-cp314-cp314-macosx_11_0_arm64.whl.
File metadata
- Download URL: solverforge-0.5.0-cp314-cp314-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.3 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 |
8ddeaa42c1d08a88b5b9a5ec13633f9ef3089529788e428019a9dbf28f99f9a7
|
|
| MD5 |
0ed04c3765e717acade3414c815dbda6
|
|
| BLAKE2b-256 |
838691ee089a86afe5b338ac37c62d9cfd49b96ba415f056e1016e288f62d40a
|