Skip to Content
Solver Integration

Solver Integration

This page documents the technical interface between StaffSchedulingWeb (the Next.js frontend) and the StaffScheduling Python solver. It covers both communication modes, the API protocol, the solver’s optimization process, and progress tracking.


Communication Modes

The web application supports two modes for communicating with the solver, configured in config.json:

The solver runs as a standalone FastAPI server on port 8000. The web application sends HTTP requests to execute operations and polls for progress.

StaffSchedulingWeb (Next.js :3000) ├── POST /solve → FastAPI Server (:8000) ├── POST /solve-multiple → FastAPI Server (:8000) ├── POST /fetch → FastAPI Server (:8000) ├── POST /insert → FastAPI Server (:8000) ├── POST /delete → FastAPI Server (:8000) └── GET /status → FastAPI Server (:8000) (polling)

Starting the API server:

cd /path/to/StaffScheduling uv run staff-scheduling-api

The server starts on http://127.0.0.1:8000 with auto-generated documentation at /docs (Swagger UI).

Implementation in the web app: src/infrastructure/services/solver-api-service.ts

Mode 2 — Direct CLI Execution

The web application spawns the Python CLI as a child process. This mode does not require a running server but blocks the Node.js process during execution.

StaffSchedulingWeb (Next.js :3000) └── child_process.exec("uv run staff-scheduling solve 77 01.11.2024 30.11.2024")

Implementation in the web app: src/infrastructure/services/python-cli-service.ts

Choosing a Mode

AspectREST APICLI
Setup complexityRequires running a separate serverNo extra process needed
Progress trackingReal-time via /status pollingNo progress feedback
Concurrent requestsSupported (async)Blocks until completion
Recommended forProduction, Workflow modeDevelopment, quick testing

Configuring the Solver Mode

The solver mode is controlled via the solverMode field in config.json:

{ "casesDirectory": "/cases", "staffSchedulingProject": { "include": true, "path": "/path/to/StaffScheduling", "pythonExecutable": "uv" }, "solverMode": "api" }
ValueBehavior
"cli"Direct CLI execution — Uses PythonCliService, spawns Python processes directly. No extra server needed. No progress tracking.
"api"REST API with auto-start — The API server is started automatically when the web app starts (npm run dev / npm run start). If already running, it is reused.

API Mode Auto-Start

When solverMode is set to "api", the system automatically manages the Python FastAPI server:

  1. App Start: On npm run dev or npm run start, the web app checks solverMode.
  2. Auto-Start: If solverMode is "api" and the API is not already running, it is spawned in the background using {pythonExecutable} run staff-scheduling-api in staffSchedulingProject.path.
  3. Reuse: If an API process is already listening on http://127.0.0.1:8000, no second process is started.
  4. Shutdown: The spawned API process is terminated automatically when the Next.js process exits.

Setting Up API Mode

To use API mode with auto-start:

  1. Update config.json:
{ "solverMode": "api", "staffSchedulingProject": { "include": true, "path": "/absolute/path/to/StaffScheduling", "pythonExecutable": "uv" } }
  1. Start the web application:
npm run dev
  1. Start the web app (npm run dev or npm run start) and the FastAPI server will be started automatically.

API Endpoints

GET /status

Returns the current solver state. The web application polls this endpoint while a solve is in progress.

Response:

{ "is_solving": true, "phase": "phase_3_optimizing", "timeout_set_for_phase_3": 300, "weight_id": 1, "total_weights": 3 }

Phase values:

PhaseDescription
"idle"Solver is not running
"phase_1_upper_bound"Finding upper bound on hidden employees
"phase_2_tight_bound"Tightening hidden employee count via binary search
"phase_3_optimizing"Full optimization with all constraints and objectives

POST /solve

Runs a single optimization with the current weight configuration.

Request:

{ "unit": 77, "start_date": "2024-11-01", "end_date": "2024-11-30", "timeout": 300 }

Response:

{ "success": true, "status": "OPTIMAL", "solution_data": { }, "log": "...", "stdout": "...", "console_output": "..." }

The solution_data field contains the complete processed solution (same format as processed_solution_*.json — see Underlying Data).

POST /solve-multiple

Runs the solver three times with different weight presets (DEFAULT, BALANCED, STAFF_FOCUS).

Request: Same as /solve.

Response:

{ "success": true, "results": [ { "weight_id": 0, "status": "OPTIMAL", "solution_data": { } }, { "weight_id": 1, "status": "FEASIBLE", "solution_data": { } }, { "weight_id": 2, "status": "OPTIMAL", "solution_data": { } } ], "log": "...", "stdout": "...", "console_output": "..." }

Phases 1 and 2 (hidden employee determination) only run once — the results are reused across all three weight configurations.

POST /fetch

Exports data from the TimeOffice database into JSON files in the case directory.

Request:

{ "planning_unit": 77, "from_date": "2024-11-01", "till_date": "2024-11-30" }

Response: { "success": true, "log": "...", "stdout": "...", "console_output": "..." }

POST /insert

Writes the selected schedule back to the TimeOffice database.

Request:

{ "planning_unit": 77, "from_date": "2024-11-01", "till_date": "2024-11-30", "solution_data": { } }

The solution_data field is optional — if omitted, the solver reads the last processed solution from disk.

POST /delete

Removes the previously inserted schedule from the TimeOffice database. Request and response format are the same as /insert.


Three-Phase Solving Process

The solver uses Google OR-Tools CP-SAT (Constraint Programming with Boolean Satisfiability). Each solve operation runs through three phases:

Phase 1 — Upper Bound on Hidden Employees

“Hidden employees” are synthetic placeholder employees added to ensure the schedule is feasible even when the real workforce is too small to meet minimum staffing requirements.

  • Incrementally adds hidden employees (5 at a time) for each category (Fachkraft, Hilfskraft, Azubi).
  • Runs a constraints-only check (no objectives, 5-second timeout) until feasibility is confirmed.
  • Maximum: 50 hidden employees total.

Phase 2 — Tight Bound on Hidden Employees

Binary search to find the minimum number of hidden employees needed:

  • Reduces one category at a time while keeping others fixed.
  • Result: the smallest number of hidden employees per category that still yields a feasible schedule.

Phase 3 — Full Optimization

Builds the complete model with all constraints and objectives, then minimizes:

minimize( Σ weight_i × penalty_i )

Runs with the configured timeout (default: 300 seconds, configurable per request).


Hard Constraints

These constraints must be satisfied — the solver returns INFEASIBLE if any are violated.

ConstraintDescription
MaxOneShiftPerDayEach employee works at most one shift per day
MinStaffingMinimum staff count per shift, per day, per employee category
TargetWorkingTimeTotal monthly minutes per employee within ±460 of target
VacationDaysAndShiftsNo work on vacation or forbidden days
MinRestTimeLate shift today → no early shift tomorrow
FreeDayAfterNightShiftPhaseFree day required after a night shift
PlannedShiftsPre-assigned shifts from TimeOffice are fixed
HierarchyOfIntermediateShiftsShift type hierarchy rules
RoundsInEarlyShiftRound-qualified employees must be assigned to early shifts

Soft Constraints (Objectives)

Penalties added to the objective function. The solver minimizes their weighted sum.

ObjectiveDefault WeightDescription
MinimizeHiddenEmployees100Minimize working time assigned to placeholder employees
MinimizeOvertime4Minimize deviation from target working hours
MaximizeEmployeeWishes3Minimize unfulfilled wish days and wish shifts
FreeDaysAfterNightShiftPhase3Encourage free days after night shift sequences
FreeDaysNearWeekend2Encourage free days adjacent to weekends
MinimizeConsecutiveNightShifts2Penalize consecutive night shifts (exponentially)
NotTooManyConsecutiveDays1Penalize more than 5 consecutive working days
EverySecondWeekendFree1Encourage alternating free weekends
RotateShiftsForward1Encourage forward rotation (Early → Late → Night)

Weight Presets

The solve-multiple command uses three built-in weight configurations:

Weight KeyDEFAULTBALANCEDSTAFF_FOCUS
hidden1005080
overtime4101
wishes335
after_night313
free_weekend250.1
consecutive_nights215
consecutive_days112
rotate120
second_weekend112

When running a single solve, the weights from the web application’s Weights page are used instead.


Progress Tracking in the Web Application

The web application implements progress tracking in features/solver/hooks/use-solver-operations.ts:

  1. A solve or solve-multiple server action is called (this blocks until the solver finishes).
  2. In parallel, the client component polls GET /api/solver/status (a Next.js API route that proxies to the Python solver’s GET /status).
  3. The SolverProgressDisplay component renders a progress bar with phase labels.
  4. When the server action returns, polling stops and the result is processed.

Important: Do not navigate away from the Solver or Workflow page while a solve is in progress. The polling and result handling depend on the component remaining mounted.


CLI Commands Reference

If you prefer to run the solver from the command line instead of through the web interface:

# Fetch data from TimeOffice uv run staff-scheduling fetch 77 01.11.2024 30.11.2024 # Single solve with default weights uv run staff-scheduling solve 77 01.11.2024 30.11.2024 # Single solve with custom weights and timeout uv run staff-scheduling solve 77 01.11.2024 30.11.2024 --weight hidden=200 --timeout 600 # Three solves with different weight presets uv run staff-scheduling solve-multiple 77 01.11.2024 30.11.2024 # Write selected schedule to TimeOffice uv run staff-scheduling insert 77 01.11.2024 30.11.2024 # Remove inserted schedule from TimeOffice uv run staff-scheduling delete 77 01.11.2024 30.11.2024

Date format: DD.MM.YYYY. The unit number corresponds to the TimeOffice planning unit ID (and the case directory name).

Last updated on