Solving routes and rosters, embedded or hosted
The solver runs in one of two deployment models, and you choose which. Embedded runs the solver inside your process. Hosted delegates to the Helixor solver service. The two return the same answer and raise the same errors. There is no default and no fallback from one to the other.
Runtime 0.3.0 or later
HelixorSolver(mode=...) ships in runtime 0.3.0. The embedded model also needs helixor-solvers 0.7, which is not part of the runtime wheel. The hosted model needs access to the Helixor solver service. See How this page was verified.
Choose the model#
| Embedded | Hosted | |
|---|---|---|
| Where it runs | In your process. No network call. | The Helixor solver service, over HTTPS. |
| Configure | HelixorSolver(mode="embedded", license=lic) | HelixorSolver(mode="hosted", base_url=..., auth=token) |
| Install | helixor-runtime[solvers] for routing (pure Python). Add [solvers-full] for rostering and the hybrid genetic search. | helixor-runtime[solvers-hosted] (Python 3.11+) |
| Authorises the solve | A licence entitled to vrp.fleet.v1 or rostering.staff.v1 | The service token |
Missing or contradictory configuration is refused with SolverConfigurationError before anything runs. For example, base_url given to the embedded model is refused, and so is a hosted model with no auth.
The answer#
solve(problem_type, problem, options=...) returns the solve contract. It has the same fields in both models:
| Field | Meaning |
|---|---|
verdict | feasible or infeasible, decided by the certificate |
solution | The routes or roster, only when the verdict is feasible. It is null otherwise; a plan that breaks a hard rule is never returned as the answer. |
shortfalls | What is short, and by how much: failed pre-solve checks, unserved or late stops, hard rules broken |
alternates | When infeasible, ranked options, each saying what it relaxes |
certificate | How the verdict was reached, including the rejected plan when there is one |
metadata | model, the engine that ran and why, the budget, and the seed and final cost |
The same typed errors come from both models:
SolverInputError: the problem is missing data. It is HTTP 422 on the service.SolverOptionError: an option or problem type is not accepted. Also 422.SolverEngineUnavailableError: the requested engine is not installed where the solve runs. It is 503 on the service.SolverServiceError: the hosted service could not be used. This one is hosted only.
Routing engines#
options.engine | What runs |
|---|---|
auto (default) | With time windows, the local search. Otherwise the hybrid genetic search when it is installed, and the local search when it is not. The choice is named in metadata.engine_reason. |
hgs | Split seeds plus hybrid genetic search, for capacity only. A request with time windows is refused rather than solved as if it had none. |
local_search | A sweep seed, then simulated annealing with ruin-and-recreate. Pure Python. Sequences stops against their time windows. |
Also available: time_limit_seconds, up to 600, and seed. Unknown options are refused.
Steps#
- Get the scripts
This page follows
use_cases/fleet_routing.pyanduse_cases/shift_rostering.pyin the public demos repository. Each script takes one switch,--mode embeddedor--mode hosted. Without it, the solve step says it did not run.python use_cases/fleet_routing.py --mode embedded HELIXOR_SOLVER_URL=https://... HELIXOR_SOLVER_TOKEN=... python use_cases/fleet_routing.py --mode hosted
- Configure the model
def configured_solver(mode, license): if mode == "embedded": return HelixorSolver(mode="embedded", license=license) if mode == "hosted": return HelixorSolver(mode="hosted", base_url=os.environ["HELIXOR_SOLVER_URL"], auth=os.environ["HELIXOR_SOLVER_TOKEN"]) return None - Solve today's deliveries as stated
One van with 40 crates of capacity, 74 crates of demand, and a kiosk that cannot be reached inside its window. The answer is
infeasible, with no plan. It names both shortfalls and ranks three relaxed alternates.Today as stated: verdict=infeasible, plan returned: False shortfall: Total demand is 74 units but the fleet carries 40 (1 vehicle(s) x 40); shortfall 34 units. ... shortfall: ferry-kiosk: the earliest possible arrival is 8.79h (depot opens 7h, direct drive 107 min) but its window closes at 7.5h; ... alternate #1: relaxes capacity 40.0 -> 74.0 alternate #2: relaxes n_vehicles 1 -> 3 alternate #3: relaxes visit_all_customers all stops -> 4 of 7 stops - Solve after the suggested changes
With a second van, and the kiosk moved to tomorrow, the local search sequences the stops against their windows:
With a second van and the kiosk moved: verdict=feasible engine: local search (auto: time windows declared: the local search sequences against them) seed 28.8 km -> optimised 23.1 km in a 2 s budget vehicle 1: school -> cafe -> market load 39, 12.8 km vehicle 2: clinic -> hotel -> bakery load 29, 10.3 km late stops: 0; not enforced by the search: ['return_to_depot_deadline', 'max_driver_hours']The hosted run prints the same lines, except
Served by: hosted. Theengineline names the engine that ran; it is abbreviated on this page. - Solve the clinic's week
The week needs 84 hours of registered-nurse cover, and the nurses can give 72.
shift_rostering.pyanswersinfeasiblein both models. No roster is returned as the solution; the ranked alternates name the rule each one breaks.[6] Solve the week Served by: embedded; engine: rostering local search verdict=infeasible, roster returned as the solution: False shortfall: Skill 'rn': demanded coverage is 84h but skill-capable employee capacity is 72h (deficit 12h); ... hard rule broken: undercovered x1 alternate #1: breaks employer_coverage x1 (hard) alternate #2: breaks employer_coverage x1 (hard) alternate #3: breaks employee_max_weekly_hours x1 (hard); employer_coverage x1 (hard)
What the solver reaches#
These are measured on published benchmark instances. An independent scorer checks every answer; it shares no code with the solver. Both models were run at the same engine and budget.
| Instance | Engine, budget | Cost | Best known |
|---|---|---|---|
| A-n32-k5 (capacitated) | hgs, 3 s | 784 | 784 |
| X-n115-k10 (capacitated) | hgs, 8 s | 12,747 | 12,747 |
| X-n101-k25 (capacitated) | hgs, 30 s | 30,584 (+10.9%) | 27,591 |
| Solomon C101, 25 customers (time windows) | local_search, 5 s | 191.8 | 191.3 |
| Solomon R101, 25 customers (time windows) | local_search, 5 s | 618.3 | 617.1 |
| Solomon R101, 100 customers (time windows) | local_search, 30 s | 1,799–1,854 (+10–13%) | 1,637.7 |
| Solomon C101, 100 customers (time windows) | local_search, 30 s | 1,179–1,194 (+43%) | 827.3 |
At 100 customers with time windows, the local search is well short of best-known. It is gated in the weekly quality run so the gap cannot grow unnoticed.
How this page was verified#
The embedded output blocks above were re-run on 2026-09-29 with the released runtime 0.3.0 wheel and helixor-solvers 0.7.0 on Python 3.12, and match line for line apart from the abbreviated engine lines. The hosted output was captured the same day from the same code, pre-release, against a local solver service process, and differed only in Served by.
The benchmark table comes from Helixor's solver quality suite. That suite runs the same requests through both models and fails if hard rules are broken, if a cost drifts past its declared tolerance, or if the two models disagree.