feat(engine): daily-mode OR-Tools VRP model #72

Merged
bart merged 1 commit from feature/engine-daily-mode into main 2026-09-11 12:30:42 +00:00
Owner

Description

This adds the planning engine: given a day's jobs, presence windows and crews, it works out which crew should do which job when, and hands back a proposed plan plus a clear reason for every job it couldn't fit in (site unreachable, crew unavailable, confidence too low, and so on). Planners will see this as the actual routing/scheduling behind a plan version once the orchestration ticket (T-E) wires it in; until then it's exercised directly through fixtures and polaris replay-run --file.

What it takes into account: which sites a vehicle is actually predicted to be at and how confident that prediction is, which crews have the right skills, any manual pins/forbids/time-fixes a planner has set, each crew's shift hours and lunch break, and travel time between stops (straight-line estimate for now — real road-network travel time is a later ticket, #42). It also tries not to reshuffle a crew's already-confirmed work unless the improvement is worth it, and keeps a crew's mandatory lunch break in the schedule even if that means turning away a lower-priority job.

Tested against three prepared scenarios: a small two-job case with a known best answer, a medium eight-job case with a mix of skills, locks and a lunch break, and a stress case that piles every kind of manual override onto the same two jobs to prove each one is actually respected.

Closes #18

PH2 / T-C in docs/12-implementation-plan.md. Scenario S04.

Type of Change

  • New feature
  • Bug fix
  • Refactor / cleanup (no behaviour change)
  • Documentation
  • Chore / build / CI
  • Breaking change

Breaking Changes

None.

Test Plan

Tests green, including a slow regression test (skipped by default, run with --run-slow) that solves the medium fixture within its real 120-second daily time budget. Also ran polaris replay-run --file by hand against all three fixtures to confirm the command still loads and reports on them cleanly end to end.

Checklist

  • Conventional commit(s), one logical change per commit
  • Tests added/updated and passing
  • docs/ updated for any behavioural change (ADR added if a prior decision was reversed)
  • No hard deletes; no direct status updates outside polaris.domain
  • No business logic in routers/components
  • Migration included if the schema changed (one per PR max, reversible downgrade) — N/A, no schema change
  • No new dependency without a reason in the commit body — N/A, ortools was already declared in PH0.2

Additional Context

Follow-up outside this ticket's owned paths: polaris/cli/replay_run.py's _solve_stub/_validate_stub still print "not built yet" — that file belongs to PH1.4 (T-C isn't allowed to touch it per the row's owned-paths, and its own test asserts the stub's exact stub output), and its docstring frames wiring it up as work for once both T-C and T-D have landed. Both have now merged, so wiring those two stub bodies to the real polaris.engine.solve/polaris.validator.validate calls is a small, ready follow-up — happy to pick it up as a tiny seam PR if no one already has it, otherwise flagging it here so it isn't lost. In the meantime this PR proves the composition directly (test_solve_composes_with_replay_runs_own_loader, plus the fixtures ran by hand through the actual CLI).

Also worth a reviewer's eyes: the installed OR-Tools build's RoutingModel.SetAllowedVehiclesForIndex — the call docs/05-planning-engine.md names for skill/lock constraints — raises a TypeError for every input shape (list, tuple, array, numpy), a broken SWIG binding rather than a usage mistake (double-checked against AddDisjunction, whose own Span argument binds fine). Used the documented-equivalent VehicleVar(node).SetValues([...]) instead everywhere; commented in vrp_model.py and noted in docs/05-planning-engine.md's constraint table so it isn't rediscovered from scratch later.

Also resolved Q10 in docs/05-planning-engine.md concretely: the mandatory break node's location is the shift's start location (crew returns to base for lunch) rather than wherever the route happens to be — flagged in the doc as worth revisiting if that assumption proves wrong in practice.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX

## Description This adds the planning engine: given a day's jobs, presence windows and crews, it works out which crew should do which job when, and hands back a proposed plan plus a clear reason for every job it couldn't fit in (site unreachable, crew unavailable, confidence too low, and so on). Planners will see this as the actual routing/scheduling behind a plan version once the orchestration ticket (T-E) wires it in; until then it's exercised directly through fixtures and `polaris replay-run --file`. What it takes into account: which sites a vehicle is actually predicted to be at and how confident that prediction is, which crews have the right skills, any manual pins/forbids/time-fixes a planner has set, each crew's shift hours and lunch break, and travel time between stops (straight-line estimate for now — real road-network travel time is a later ticket, #42). It also tries not to reshuffle a crew's already-confirmed work unless the improvement is worth it, and keeps a crew's mandatory lunch break in the schedule even if that means turning away a lower-priority job. Tested against three prepared scenarios: a small two-job case with a known best answer, a medium eight-job case with a mix of skills, locks and a lunch break, and a stress case that piles every kind of manual override onto the same two jobs to prove each one is actually respected. ## Related Issues Closes #18 PH2 / T-C in `docs/12-implementation-plan.md`. Scenario S04. ## Type of Change - [x] New feature - [ ] Bug fix - [ ] Refactor / cleanup (no behaviour change) - [ ] Documentation - [ ] Chore / build / CI - [ ] Breaking change ## Breaking Changes None. ## Test Plan Tests green, including a slow regression test (skipped by default, run with `--run-slow`) that solves the medium fixture within its real 120-second daily time budget. Also ran `polaris replay-run --file` by hand against all three fixtures to confirm the command still loads and reports on them cleanly end to end. ## Checklist - [x] Conventional commit(s), one logical change per commit - [x] Tests added/updated and passing - [x] `docs/` updated for any behavioural change (ADR added if a prior decision was reversed) - [x] No hard deletes; no direct status updates outside `polaris.domain` - [x] No business logic in routers/components - [x] Migration included if the schema changed (one per PR max, reversible downgrade) — N/A, no schema change - [x] No new dependency without a reason in the commit body — N/A, `ortools` was already declared in PH0.2 ## Additional Context **Follow-up outside this ticket's owned paths**: `polaris/cli/replay_run.py`'s `_solve_stub`/`_validate_stub` still print "not built yet" — that file belongs to PH1.4 (T-C isn't allowed to touch it per the row's owned-paths, and its own test asserts the stub's exact stub output), and its docstring frames wiring it up as work for once *both* T-C and T-D have landed. Both have now merged, so wiring those two stub bodies to the real `polaris.engine.solve`/`polaris.validator.validate` calls is a small, ready follow-up — happy to pick it up as a tiny seam PR if no one already has it, otherwise flagging it here so it isn't lost. In the meantime this PR proves the composition directly (`test_solve_composes_with_replay_runs_own_loader`, plus the fixtures ran by hand through the actual CLI). **Also worth a reviewer's eyes**: the installed OR-Tools build's `RoutingModel.SetAllowedVehiclesForIndex` — the call `docs/05-planning-engine.md` names for skill/lock constraints — raises a `TypeError` for every input shape (list, tuple, array, numpy), a broken SWIG binding rather than a usage mistake (double-checked against `AddDisjunction`, whose own `Span` argument binds fine). Used the documented-equivalent `VehicleVar(node).SetValues([...])` instead everywhere; commented in `vrp_model.py` and noted in `docs/05-planning-engine.md`'s constraint table so it isn't rediscovered from scratch later. Also resolved Q10 in `docs/05-planning-engine.md` concretely: the mandatory break node's location is the shift's start location (crew returns to base for lunch) rather than wherever the route happens to be — flagged in the doc as worth revisiting if that assumption proves wrong in practice. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX
Implements the T-C planning engine (PH2): opportunity generation (job x
presence-window pairs passing suitability, confidence, duration and lock
checks per docs/05-planning-engine.md §2), a TravelMatrixProvider protocol
with a haversine fallback implementation (OSRM lands as a drop-in later,
#42), the OR-Tools VRP model for daily mode (routing dimensions, skill
constraints, all eleven lock types, node time windows, mandatory break
nodes per Q10, soft-locked confirmed assignments with move/time-shift
penalties, search per §6), and output mapping to PlanResult including the
closed unassigned-reason list.

Fixture instances (tiny, medium, every-lock-type) solve correctly; a slow
regression test proves the medium fixture within its real 120s daily
budget. polaris replay-run --file composes cleanly with the new engine
(cli/replay_run.py's stub wiring is PH1.4's file, left untouched per file
ownership — see the PR description).

Works around a broken OR-Tools 9.12-9.15 SetAllowedVehiclesForIndex SWIG
binding by using the documented-equivalent VehicleVar(...).SetValues(...)
call instead (see the comment in vrp_model.py).

Closes #18

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX
bart merged commit 11eb03652c into main 2026-09-11 12:30:42 +00:00
bart deleted branch feature/engine-daily-mode 2026-09-11 12:30:43 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
patrick/Polaris!72
No description provided.