Spaces:
Runtime error
Runtime error
Upload 30 files
Browse files- Cargo.lock +1476 -0
- Cargo.toml +21 -0
- Dockerfile +42 -0
- README.md +91 -6
- helm/employee-scheduling/.helmignore +17 -0
- helm/employee-scheduling/Chart.yaml +12 -0
- helm/employee-scheduling/templates/NOTES.txt +31 -0
- helm/employee-scheduling/templates/_helpers.tpl +49 -0
- helm/employee-scheduling/templates/deployment.yaml +67 -0
- helm/employee-scheduling/templates/ingress.yaml +41 -0
- helm/employee-scheduling/templates/service.yaml +15 -0
- helm/employee-scheduling/values.yaml +72 -0
- solver.toml +5 -0
- src/api.rs +425 -0
- src/bin/bench.rs +65 -0
- src/console.rs +389 -0
- src/constraints.rs +228 -0
- src/demo_data.rs +370 -0
- src/domain.rs +175 -0
- src/lib.rs +11 -0
- src/main.rs +40 -0
- src/solver.rs +562 -0
- static/app.js +522 -0
- static/index.html +173 -0
- static/webjars/solverforge/css/solverforge-webui.css +68 -0
- static/webjars/solverforge/img/solverforge-favicon.svg +65 -0
- static/webjars/solverforge/img/solverforge-horizontal-white.svg +66 -0
- static/webjars/solverforge/img/solverforge-horizontal.svg +65 -0
- static/webjars/solverforge/img/solverforge-logo-stacked.svg +73 -0
- static/webjars/solverforge/js/solverforge-webui.js +142 -0
Cargo.lock
ADDED
|
@@ -0,0 +1,1476 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file is automatically @generated by Cargo.
|
| 2 |
+
# It is not intended for manual editing.
|
| 3 |
+
version = 4
|
| 4 |
+
|
| 5 |
+
[[package]]
|
| 6 |
+
name = "aho-corasick"
|
| 7 |
+
version = "1.1.4"
|
| 8 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 9 |
+
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"memchr",
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
[[package]]
|
| 15 |
+
name = "android_system_properties"
|
| 16 |
+
version = "0.1.5"
|
| 17 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 18 |
+
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
| 19 |
+
dependencies = [
|
| 20 |
+
"libc",
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
[[package]]
|
| 24 |
+
name = "arrayvec"
|
| 25 |
+
version = "0.7.6"
|
| 26 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 27 |
+
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
| 28 |
+
|
| 29 |
+
[[package]]
|
| 30 |
+
name = "atomic-waker"
|
| 31 |
+
version = "1.1.2"
|
| 32 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 33 |
+
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
| 34 |
+
|
| 35 |
+
[[package]]
|
| 36 |
+
name = "autocfg"
|
| 37 |
+
version = "1.5.0"
|
| 38 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 39 |
+
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
| 40 |
+
|
| 41 |
+
[[package]]
|
| 42 |
+
name = "axum"
|
| 43 |
+
version = "0.8.8"
|
| 44 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 45 |
+
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
| 46 |
+
dependencies = [
|
| 47 |
+
"axum-core",
|
| 48 |
+
"bytes",
|
| 49 |
+
"form_urlencoded",
|
| 50 |
+
"futures-util",
|
| 51 |
+
"http",
|
| 52 |
+
"http-body",
|
| 53 |
+
"http-body-util",
|
| 54 |
+
"hyper",
|
| 55 |
+
"hyper-util",
|
| 56 |
+
"itoa",
|
| 57 |
+
"matchit",
|
| 58 |
+
"memchr",
|
| 59 |
+
"mime",
|
| 60 |
+
"percent-encoding",
|
| 61 |
+
"pin-project-lite",
|
| 62 |
+
"serde_core",
|
| 63 |
+
"serde_json",
|
| 64 |
+
"serde_path_to_error",
|
| 65 |
+
"serde_urlencoded",
|
| 66 |
+
"sync_wrapper",
|
| 67 |
+
"tokio",
|
| 68 |
+
"tower",
|
| 69 |
+
"tower-layer",
|
| 70 |
+
"tower-service",
|
| 71 |
+
"tracing",
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
[[package]]
|
| 75 |
+
name = "axum-core"
|
| 76 |
+
version = "0.5.6"
|
| 77 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 78 |
+
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
| 79 |
+
dependencies = [
|
| 80 |
+
"bytes",
|
| 81 |
+
"futures-core",
|
| 82 |
+
"http",
|
| 83 |
+
"http-body",
|
| 84 |
+
"http-body-util",
|
| 85 |
+
"mime",
|
| 86 |
+
"pin-project-lite",
|
| 87 |
+
"sync_wrapper",
|
| 88 |
+
"tower-layer",
|
| 89 |
+
"tower-service",
|
| 90 |
+
"tracing",
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
+
[[package]]
|
| 94 |
+
name = "bitflags"
|
| 95 |
+
version = "2.10.0"
|
| 96 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 97 |
+
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
| 98 |
+
|
| 99 |
+
[[package]]
|
| 100 |
+
name = "bumpalo"
|
| 101 |
+
version = "3.19.1"
|
| 102 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 103 |
+
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
| 104 |
+
|
| 105 |
+
[[package]]
|
| 106 |
+
name = "bytes"
|
| 107 |
+
version = "1.11.0"
|
| 108 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 109 |
+
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
| 110 |
+
|
| 111 |
+
[[package]]
|
| 112 |
+
name = "cc"
|
| 113 |
+
version = "1.2.51"
|
| 114 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 115 |
+
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
|
| 116 |
+
dependencies = [
|
| 117 |
+
"find-msvc-tools",
|
| 118 |
+
"shlex",
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
[[package]]
|
| 122 |
+
name = "cfg-if"
|
| 123 |
+
version = "1.0.4"
|
| 124 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 125 |
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
| 126 |
+
|
| 127 |
+
[[package]]
|
| 128 |
+
name = "chrono"
|
| 129 |
+
version = "0.4.42"
|
| 130 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 131 |
+
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
| 132 |
+
dependencies = [
|
| 133 |
+
"iana-time-zone",
|
| 134 |
+
"js-sys",
|
| 135 |
+
"num-traits",
|
| 136 |
+
"serde",
|
| 137 |
+
"wasm-bindgen",
|
| 138 |
+
"windows-link",
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
[[package]]
|
| 142 |
+
name = "core-foundation-sys"
|
| 143 |
+
version = "0.8.7"
|
| 144 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 145 |
+
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
| 146 |
+
|
| 147 |
+
[[package]]
|
| 148 |
+
name = "crossbeam-deque"
|
| 149 |
+
version = "0.8.6"
|
| 150 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 151 |
+
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
| 152 |
+
dependencies = [
|
| 153 |
+
"crossbeam-epoch",
|
| 154 |
+
"crossbeam-utils",
|
| 155 |
+
]
|
| 156 |
+
|
| 157 |
+
[[package]]
|
| 158 |
+
name = "crossbeam-epoch"
|
| 159 |
+
version = "0.9.18"
|
| 160 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 161 |
+
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
| 162 |
+
dependencies = [
|
| 163 |
+
"crossbeam-utils",
|
| 164 |
+
]
|
| 165 |
+
|
| 166 |
+
[[package]]
|
| 167 |
+
name = "crossbeam-utils"
|
| 168 |
+
version = "0.8.21"
|
| 169 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 170 |
+
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
| 171 |
+
|
| 172 |
+
[[package]]
|
| 173 |
+
name = "either"
|
| 174 |
+
version = "1.15.0"
|
| 175 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 176 |
+
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
| 177 |
+
|
| 178 |
+
[[package]]
|
| 179 |
+
name = "employee-scheduling"
|
| 180 |
+
version = "0.5.0"
|
| 181 |
+
dependencies = [
|
| 182 |
+
"axum",
|
| 183 |
+
"chrono",
|
| 184 |
+
"parking_lot",
|
| 185 |
+
"rand 0.8.5",
|
| 186 |
+
"rayon",
|
| 187 |
+
"serde",
|
| 188 |
+
"serde_json",
|
| 189 |
+
"solverforge",
|
| 190 |
+
"tokio",
|
| 191 |
+
"tower",
|
| 192 |
+
"tower-http",
|
| 193 |
+
"uuid",
|
| 194 |
+
]
|
| 195 |
+
|
| 196 |
+
[[package]]
|
| 197 |
+
name = "equivalent"
|
| 198 |
+
version = "1.0.2"
|
| 199 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 200 |
+
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
| 201 |
+
|
| 202 |
+
[[package]]
|
| 203 |
+
name = "errno"
|
| 204 |
+
version = "0.3.14"
|
| 205 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 206 |
+
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
| 207 |
+
dependencies = [
|
| 208 |
+
"libc",
|
| 209 |
+
"windows-sys 0.61.2",
|
| 210 |
+
]
|
| 211 |
+
|
| 212 |
+
[[package]]
|
| 213 |
+
name = "find-msvc-tools"
|
| 214 |
+
version = "0.1.6"
|
| 215 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 216 |
+
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
| 217 |
+
|
| 218 |
+
[[package]]
|
| 219 |
+
name = "form_urlencoded"
|
| 220 |
+
version = "1.2.2"
|
| 221 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 222 |
+
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
| 223 |
+
dependencies = [
|
| 224 |
+
"percent-encoding",
|
| 225 |
+
]
|
| 226 |
+
|
| 227 |
+
[[package]]
|
| 228 |
+
name = "futures-channel"
|
| 229 |
+
version = "0.3.31"
|
| 230 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 231 |
+
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
| 232 |
+
dependencies = [
|
| 233 |
+
"futures-core",
|
| 234 |
+
]
|
| 235 |
+
|
| 236 |
+
[[package]]
|
| 237 |
+
name = "futures-core"
|
| 238 |
+
version = "0.3.31"
|
| 239 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 240 |
+
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
| 241 |
+
|
| 242 |
+
[[package]]
|
| 243 |
+
name = "futures-sink"
|
| 244 |
+
version = "0.3.31"
|
| 245 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 246 |
+
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
| 247 |
+
|
| 248 |
+
[[package]]
|
| 249 |
+
name = "futures-task"
|
| 250 |
+
version = "0.3.31"
|
| 251 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 252 |
+
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
| 253 |
+
|
| 254 |
+
[[package]]
|
| 255 |
+
name = "futures-util"
|
| 256 |
+
version = "0.3.31"
|
| 257 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 258 |
+
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
| 259 |
+
dependencies = [
|
| 260 |
+
"futures-core",
|
| 261 |
+
"futures-task",
|
| 262 |
+
"pin-project-lite",
|
| 263 |
+
"pin-utils",
|
| 264 |
+
]
|
| 265 |
+
|
| 266 |
+
[[package]]
|
| 267 |
+
name = "getrandom"
|
| 268 |
+
version = "0.2.16"
|
| 269 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 270 |
+
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
| 271 |
+
dependencies = [
|
| 272 |
+
"cfg-if",
|
| 273 |
+
"libc",
|
| 274 |
+
"wasi",
|
| 275 |
+
]
|
| 276 |
+
|
| 277 |
+
[[package]]
|
| 278 |
+
name = "getrandom"
|
| 279 |
+
version = "0.3.4"
|
| 280 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 281 |
+
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
| 282 |
+
dependencies = [
|
| 283 |
+
"cfg-if",
|
| 284 |
+
"libc",
|
| 285 |
+
"r-efi",
|
| 286 |
+
"wasip2",
|
| 287 |
+
]
|
| 288 |
+
|
| 289 |
+
[[package]]
|
| 290 |
+
name = "hashbrown"
|
| 291 |
+
version = "0.16.1"
|
| 292 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 293 |
+
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
| 294 |
+
|
| 295 |
+
[[package]]
|
| 296 |
+
name = "http"
|
| 297 |
+
version = "1.4.0"
|
| 298 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 299 |
+
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
| 300 |
+
dependencies = [
|
| 301 |
+
"bytes",
|
| 302 |
+
"itoa",
|
| 303 |
+
]
|
| 304 |
+
|
| 305 |
+
[[package]]
|
| 306 |
+
name = "http-body"
|
| 307 |
+
version = "1.0.1"
|
| 308 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 309 |
+
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
| 310 |
+
dependencies = [
|
| 311 |
+
"bytes",
|
| 312 |
+
"http",
|
| 313 |
+
]
|
| 314 |
+
|
| 315 |
+
[[package]]
|
| 316 |
+
name = "http-body-util"
|
| 317 |
+
version = "0.1.3"
|
| 318 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 319 |
+
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
| 320 |
+
dependencies = [
|
| 321 |
+
"bytes",
|
| 322 |
+
"futures-core",
|
| 323 |
+
"http",
|
| 324 |
+
"http-body",
|
| 325 |
+
"pin-project-lite",
|
| 326 |
+
]
|
| 327 |
+
|
| 328 |
+
[[package]]
|
| 329 |
+
name = "http-range-header"
|
| 330 |
+
version = "0.4.2"
|
| 331 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 332 |
+
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
| 333 |
+
|
| 334 |
+
[[package]]
|
| 335 |
+
name = "httparse"
|
| 336 |
+
version = "1.10.1"
|
| 337 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 338 |
+
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
| 339 |
+
|
| 340 |
+
[[package]]
|
| 341 |
+
name = "httpdate"
|
| 342 |
+
version = "1.0.3"
|
| 343 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 344 |
+
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
| 345 |
+
|
| 346 |
+
[[package]]
|
| 347 |
+
name = "hyper"
|
| 348 |
+
version = "1.8.1"
|
| 349 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 350 |
+
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
| 351 |
+
dependencies = [
|
| 352 |
+
"atomic-waker",
|
| 353 |
+
"bytes",
|
| 354 |
+
"futures-channel",
|
| 355 |
+
"futures-core",
|
| 356 |
+
"http",
|
| 357 |
+
"http-body",
|
| 358 |
+
"httparse",
|
| 359 |
+
"httpdate",
|
| 360 |
+
"itoa",
|
| 361 |
+
"pin-project-lite",
|
| 362 |
+
"pin-utils",
|
| 363 |
+
"smallvec",
|
| 364 |
+
"tokio",
|
| 365 |
+
]
|
| 366 |
+
|
| 367 |
+
[[package]]
|
| 368 |
+
name = "hyper-util"
|
| 369 |
+
version = "0.1.19"
|
| 370 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 371 |
+
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
| 372 |
+
dependencies = [
|
| 373 |
+
"bytes",
|
| 374 |
+
"futures-core",
|
| 375 |
+
"http",
|
| 376 |
+
"http-body",
|
| 377 |
+
"hyper",
|
| 378 |
+
"pin-project-lite",
|
| 379 |
+
"tokio",
|
| 380 |
+
"tower-service",
|
| 381 |
+
]
|
| 382 |
+
|
| 383 |
+
[[package]]
|
| 384 |
+
name = "iana-time-zone"
|
| 385 |
+
version = "0.1.64"
|
| 386 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 387 |
+
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
| 388 |
+
dependencies = [
|
| 389 |
+
"android_system_properties",
|
| 390 |
+
"core-foundation-sys",
|
| 391 |
+
"iana-time-zone-haiku",
|
| 392 |
+
"js-sys",
|
| 393 |
+
"log",
|
| 394 |
+
"wasm-bindgen",
|
| 395 |
+
"windows-core",
|
| 396 |
+
]
|
| 397 |
+
|
| 398 |
+
[[package]]
|
| 399 |
+
name = "iana-time-zone-haiku"
|
| 400 |
+
version = "0.1.2"
|
| 401 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 402 |
+
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
| 403 |
+
dependencies = [
|
| 404 |
+
"cc",
|
| 405 |
+
]
|
| 406 |
+
|
| 407 |
+
[[package]]
|
| 408 |
+
name = "indexmap"
|
| 409 |
+
version = "2.12.1"
|
| 410 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 411 |
+
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
| 412 |
+
dependencies = [
|
| 413 |
+
"equivalent",
|
| 414 |
+
"hashbrown",
|
| 415 |
+
]
|
| 416 |
+
|
| 417 |
+
[[package]]
|
| 418 |
+
name = "itoa"
|
| 419 |
+
version = "1.0.17"
|
| 420 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 421 |
+
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
| 422 |
+
|
| 423 |
+
[[package]]
|
| 424 |
+
name = "js-sys"
|
| 425 |
+
version = "0.3.83"
|
| 426 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 427 |
+
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
| 428 |
+
dependencies = [
|
| 429 |
+
"once_cell",
|
| 430 |
+
"wasm-bindgen",
|
| 431 |
+
]
|
| 432 |
+
|
| 433 |
+
[[package]]
|
| 434 |
+
name = "lazy_static"
|
| 435 |
+
version = "1.5.0"
|
| 436 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 437 |
+
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
| 438 |
+
|
| 439 |
+
[[package]]
|
| 440 |
+
name = "libc"
|
| 441 |
+
version = "0.2.179"
|
| 442 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 443 |
+
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
|
| 444 |
+
|
| 445 |
+
[[package]]
|
| 446 |
+
name = "lock_api"
|
| 447 |
+
version = "0.4.14"
|
| 448 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 449 |
+
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
| 450 |
+
dependencies = [
|
| 451 |
+
"scopeguard",
|
| 452 |
+
]
|
| 453 |
+
|
| 454 |
+
[[package]]
|
| 455 |
+
name = "log"
|
| 456 |
+
version = "0.4.29"
|
| 457 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 458 |
+
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
| 459 |
+
|
| 460 |
+
[[package]]
|
| 461 |
+
name = "matchers"
|
| 462 |
+
version = "0.2.0"
|
| 463 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 464 |
+
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
| 465 |
+
dependencies = [
|
| 466 |
+
"regex-automata",
|
| 467 |
+
]
|
| 468 |
+
|
| 469 |
+
[[package]]
|
| 470 |
+
name = "matchit"
|
| 471 |
+
version = "0.8.4"
|
| 472 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 473 |
+
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
| 474 |
+
|
| 475 |
+
[[package]]
|
| 476 |
+
name = "memchr"
|
| 477 |
+
version = "2.7.6"
|
| 478 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 479 |
+
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
| 480 |
+
|
| 481 |
+
[[package]]
|
| 482 |
+
name = "mime"
|
| 483 |
+
version = "0.3.17"
|
| 484 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 485 |
+
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
| 486 |
+
|
| 487 |
+
[[package]]
|
| 488 |
+
name = "mime_guess"
|
| 489 |
+
version = "2.0.5"
|
| 490 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 491 |
+
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
| 492 |
+
dependencies = [
|
| 493 |
+
"mime",
|
| 494 |
+
"unicase",
|
| 495 |
+
]
|
| 496 |
+
|
| 497 |
+
[[package]]
|
| 498 |
+
name = "mio"
|
| 499 |
+
version = "1.1.1"
|
| 500 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 501 |
+
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
| 502 |
+
dependencies = [
|
| 503 |
+
"libc",
|
| 504 |
+
"wasi",
|
| 505 |
+
"windows-sys 0.61.2",
|
| 506 |
+
]
|
| 507 |
+
|
| 508 |
+
[[package]]
|
| 509 |
+
name = "nu-ansi-term"
|
| 510 |
+
version = "0.50.3"
|
| 511 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 512 |
+
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
| 513 |
+
dependencies = [
|
| 514 |
+
"windows-sys 0.61.2",
|
| 515 |
+
]
|
| 516 |
+
|
| 517 |
+
[[package]]
|
| 518 |
+
name = "num-format"
|
| 519 |
+
version = "0.4.4"
|
| 520 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 521 |
+
checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
|
| 522 |
+
dependencies = [
|
| 523 |
+
"arrayvec",
|
| 524 |
+
"itoa",
|
| 525 |
+
]
|
| 526 |
+
|
| 527 |
+
[[package]]
|
| 528 |
+
name = "num-traits"
|
| 529 |
+
version = "0.2.19"
|
| 530 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 531 |
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
| 532 |
+
dependencies = [
|
| 533 |
+
"autocfg",
|
| 534 |
+
]
|
| 535 |
+
|
| 536 |
+
[[package]]
|
| 537 |
+
name = "once_cell"
|
| 538 |
+
version = "1.21.3"
|
| 539 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 540 |
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
| 541 |
+
|
| 542 |
+
[[package]]
|
| 543 |
+
name = "owo-colors"
|
| 544 |
+
version = "4.2.3"
|
| 545 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 546 |
+
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
|
| 547 |
+
|
| 548 |
+
[[package]]
|
| 549 |
+
name = "parking_lot"
|
| 550 |
+
version = "0.12.5"
|
| 551 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 552 |
+
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
| 553 |
+
dependencies = [
|
| 554 |
+
"lock_api",
|
| 555 |
+
"parking_lot_core",
|
| 556 |
+
]
|
| 557 |
+
|
| 558 |
+
[[package]]
|
| 559 |
+
name = "parking_lot_core"
|
| 560 |
+
version = "0.9.12"
|
| 561 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 562 |
+
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
| 563 |
+
dependencies = [
|
| 564 |
+
"cfg-if",
|
| 565 |
+
"libc",
|
| 566 |
+
"redox_syscall",
|
| 567 |
+
"smallvec",
|
| 568 |
+
"windows-link",
|
| 569 |
+
]
|
| 570 |
+
|
| 571 |
+
[[package]]
|
| 572 |
+
name = "percent-encoding"
|
| 573 |
+
version = "2.3.2"
|
| 574 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 575 |
+
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
| 576 |
+
|
| 577 |
+
[[package]]
|
| 578 |
+
name = "pin-project-lite"
|
| 579 |
+
version = "0.2.16"
|
| 580 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 581 |
+
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
| 582 |
+
|
| 583 |
+
[[package]]
|
| 584 |
+
name = "pin-utils"
|
| 585 |
+
version = "0.1.0"
|
| 586 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 587 |
+
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
| 588 |
+
|
| 589 |
+
[[package]]
|
| 590 |
+
name = "ppv-lite86"
|
| 591 |
+
version = "0.2.21"
|
| 592 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 593 |
+
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
| 594 |
+
dependencies = [
|
| 595 |
+
"zerocopy",
|
| 596 |
+
]
|
| 597 |
+
|
| 598 |
+
[[package]]
|
| 599 |
+
name = "proc-macro2"
|
| 600 |
+
version = "1.0.104"
|
| 601 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 602 |
+
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
| 603 |
+
dependencies = [
|
| 604 |
+
"unicode-ident",
|
| 605 |
+
]
|
| 606 |
+
|
| 607 |
+
[[package]]
|
| 608 |
+
name = "quote"
|
| 609 |
+
version = "1.0.42"
|
| 610 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 611 |
+
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
| 612 |
+
dependencies = [
|
| 613 |
+
"proc-macro2",
|
| 614 |
+
]
|
| 615 |
+
|
| 616 |
+
[[package]]
|
| 617 |
+
name = "r-efi"
|
| 618 |
+
version = "5.3.0"
|
| 619 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 620 |
+
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
| 621 |
+
|
| 622 |
+
[[package]]
|
| 623 |
+
name = "rand"
|
| 624 |
+
version = "0.8.5"
|
| 625 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 626 |
+
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
| 627 |
+
dependencies = [
|
| 628 |
+
"libc",
|
| 629 |
+
"rand_chacha 0.3.1",
|
| 630 |
+
"rand_core 0.6.4",
|
| 631 |
+
]
|
| 632 |
+
|
| 633 |
+
[[package]]
|
| 634 |
+
name = "rand"
|
| 635 |
+
version = "0.9.2"
|
| 636 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 637 |
+
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
| 638 |
+
dependencies = [
|
| 639 |
+
"rand_chacha 0.9.0",
|
| 640 |
+
"rand_core 0.9.3",
|
| 641 |
+
]
|
| 642 |
+
|
| 643 |
+
[[package]]
|
| 644 |
+
name = "rand_chacha"
|
| 645 |
+
version = "0.3.1"
|
| 646 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 647 |
+
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
| 648 |
+
dependencies = [
|
| 649 |
+
"ppv-lite86",
|
| 650 |
+
"rand_core 0.6.4",
|
| 651 |
+
]
|
| 652 |
+
|
| 653 |
+
[[package]]
|
| 654 |
+
name = "rand_chacha"
|
| 655 |
+
version = "0.9.0"
|
| 656 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 657 |
+
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
| 658 |
+
dependencies = [
|
| 659 |
+
"ppv-lite86",
|
| 660 |
+
"rand_core 0.9.3",
|
| 661 |
+
]
|
| 662 |
+
|
| 663 |
+
[[package]]
|
| 664 |
+
name = "rand_core"
|
| 665 |
+
version = "0.6.4"
|
| 666 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 667 |
+
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
| 668 |
+
dependencies = [
|
| 669 |
+
"getrandom 0.2.16",
|
| 670 |
+
]
|
| 671 |
+
|
| 672 |
+
[[package]]
|
| 673 |
+
name = "rand_core"
|
| 674 |
+
version = "0.9.3"
|
| 675 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 676 |
+
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
| 677 |
+
dependencies = [
|
| 678 |
+
"getrandom 0.3.4",
|
| 679 |
+
]
|
| 680 |
+
|
| 681 |
+
[[package]]
|
| 682 |
+
name = "rayon"
|
| 683 |
+
version = "1.11.0"
|
| 684 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 685 |
+
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
| 686 |
+
dependencies = [
|
| 687 |
+
"either",
|
| 688 |
+
"rayon-core",
|
| 689 |
+
]
|
| 690 |
+
|
| 691 |
+
[[package]]
|
| 692 |
+
name = "rayon-core"
|
| 693 |
+
version = "1.13.0"
|
| 694 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 695 |
+
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
| 696 |
+
dependencies = [
|
| 697 |
+
"crossbeam-deque",
|
| 698 |
+
"crossbeam-utils",
|
| 699 |
+
]
|
| 700 |
+
|
| 701 |
+
[[package]]
|
| 702 |
+
name = "redox_syscall"
|
| 703 |
+
version = "0.5.18"
|
| 704 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 705 |
+
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
| 706 |
+
dependencies = [
|
| 707 |
+
"bitflags",
|
| 708 |
+
]
|
| 709 |
+
|
| 710 |
+
[[package]]
|
| 711 |
+
name = "regex-automata"
|
| 712 |
+
version = "0.4.13"
|
| 713 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 714 |
+
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
| 715 |
+
dependencies = [
|
| 716 |
+
"aho-corasick",
|
| 717 |
+
"memchr",
|
| 718 |
+
"regex-syntax",
|
| 719 |
+
]
|
| 720 |
+
|
| 721 |
+
[[package]]
|
| 722 |
+
name = "regex-syntax"
|
| 723 |
+
version = "0.8.8"
|
| 724 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 725 |
+
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
| 726 |
+
|
| 727 |
+
[[package]]
|
| 728 |
+
name = "rustversion"
|
| 729 |
+
version = "1.0.22"
|
| 730 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 731 |
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
| 732 |
+
|
| 733 |
+
[[package]]
|
| 734 |
+
name = "ryu"
|
| 735 |
+
version = "1.0.22"
|
| 736 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 737 |
+
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
| 738 |
+
|
| 739 |
+
[[package]]
|
| 740 |
+
name = "scopeguard"
|
| 741 |
+
version = "1.2.0"
|
| 742 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 743 |
+
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
| 744 |
+
|
| 745 |
+
[[package]]
|
| 746 |
+
name = "serde"
|
| 747 |
+
version = "1.0.228"
|
| 748 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 749 |
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
| 750 |
+
dependencies = [
|
| 751 |
+
"serde_core",
|
| 752 |
+
"serde_derive",
|
| 753 |
+
]
|
| 754 |
+
|
| 755 |
+
[[package]]
|
| 756 |
+
name = "serde_core"
|
| 757 |
+
version = "1.0.228"
|
| 758 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 759 |
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
| 760 |
+
dependencies = [
|
| 761 |
+
"serde_derive",
|
| 762 |
+
]
|
| 763 |
+
|
| 764 |
+
[[package]]
|
| 765 |
+
name = "serde_derive"
|
| 766 |
+
version = "1.0.228"
|
| 767 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 768 |
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
| 769 |
+
dependencies = [
|
| 770 |
+
"proc-macro2",
|
| 771 |
+
"quote",
|
| 772 |
+
"syn",
|
| 773 |
+
]
|
| 774 |
+
|
| 775 |
+
[[package]]
|
| 776 |
+
name = "serde_json"
|
| 777 |
+
version = "1.0.148"
|
| 778 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 779 |
+
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
| 780 |
+
dependencies = [
|
| 781 |
+
"itoa",
|
| 782 |
+
"memchr",
|
| 783 |
+
"serde",
|
| 784 |
+
"serde_core",
|
| 785 |
+
"zmij",
|
| 786 |
+
]
|
| 787 |
+
|
| 788 |
+
[[package]]
|
| 789 |
+
name = "serde_path_to_error"
|
| 790 |
+
version = "0.1.20"
|
| 791 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 792 |
+
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
| 793 |
+
dependencies = [
|
| 794 |
+
"itoa",
|
| 795 |
+
"serde",
|
| 796 |
+
"serde_core",
|
| 797 |
+
]
|
| 798 |
+
|
| 799 |
+
[[package]]
|
| 800 |
+
name = "serde_spanned"
|
| 801 |
+
version = "0.6.9"
|
| 802 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 803 |
+
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
| 804 |
+
dependencies = [
|
| 805 |
+
"serde",
|
| 806 |
+
]
|
| 807 |
+
|
| 808 |
+
[[package]]
|
| 809 |
+
name = "serde_urlencoded"
|
| 810 |
+
version = "0.7.1"
|
| 811 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 812 |
+
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
| 813 |
+
dependencies = [
|
| 814 |
+
"form_urlencoded",
|
| 815 |
+
"itoa",
|
| 816 |
+
"ryu",
|
| 817 |
+
"serde",
|
| 818 |
+
]
|
| 819 |
+
|
| 820 |
+
[[package]]
|
| 821 |
+
name = "serde_yaml"
|
| 822 |
+
version = "0.9.34+deprecated"
|
| 823 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 824 |
+
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
| 825 |
+
dependencies = [
|
| 826 |
+
"indexmap",
|
| 827 |
+
"itoa",
|
| 828 |
+
"ryu",
|
| 829 |
+
"serde",
|
| 830 |
+
"unsafe-libyaml",
|
| 831 |
+
]
|
| 832 |
+
|
| 833 |
+
[[package]]
|
| 834 |
+
name = "sharded-slab"
|
| 835 |
+
version = "0.1.7"
|
| 836 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 837 |
+
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
| 838 |
+
dependencies = [
|
| 839 |
+
"lazy_static",
|
| 840 |
+
]
|
| 841 |
+
|
| 842 |
+
[[package]]
|
| 843 |
+
name = "shlex"
|
| 844 |
+
version = "1.3.0"
|
| 845 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 846 |
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
| 847 |
+
|
| 848 |
+
[[package]]
|
| 849 |
+
name = "signal-hook-registry"
|
| 850 |
+
version = "1.4.8"
|
| 851 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 852 |
+
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
| 853 |
+
dependencies = [
|
| 854 |
+
"errno",
|
| 855 |
+
"libc",
|
| 856 |
+
]
|
| 857 |
+
|
| 858 |
+
[[package]]
|
| 859 |
+
name = "smallvec"
|
| 860 |
+
version = "1.15.1"
|
| 861 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 862 |
+
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
| 863 |
+
|
| 864 |
+
[[package]]
|
| 865 |
+
name = "socket2"
|
| 866 |
+
version = "0.6.1"
|
| 867 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 868 |
+
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
| 869 |
+
dependencies = [
|
| 870 |
+
"libc",
|
| 871 |
+
"windows-sys 0.60.2",
|
| 872 |
+
]
|
| 873 |
+
|
| 874 |
+
[[package]]
|
| 875 |
+
name = "solverforge"
|
| 876 |
+
version = "0.5.0"
|
| 877 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 878 |
+
checksum = "bb513309e056520689d2e85b5b0cd41bfe683dbf8cb2e9551e8251a38fa7dbdd"
|
| 879 |
+
dependencies = [
|
| 880 |
+
"num-format",
|
| 881 |
+
"owo-colors",
|
| 882 |
+
"solverforge-config",
|
| 883 |
+
"solverforge-core",
|
| 884 |
+
"solverforge-macros",
|
| 885 |
+
"solverforge-scoring",
|
| 886 |
+
"solverforge-solver",
|
| 887 |
+
"tracing",
|
| 888 |
+
"tracing-subscriber",
|
| 889 |
+
]
|
| 890 |
+
|
| 891 |
+
[[package]]
|
| 892 |
+
name = "solverforge-config"
|
| 893 |
+
version = "0.5.0"
|
| 894 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 895 |
+
checksum = "dd73530a3b29a90f34778f7326d812605bf2deb4c253027fffef39a587bf1880"
|
| 896 |
+
dependencies = [
|
| 897 |
+
"serde",
|
| 898 |
+
"serde_yaml",
|
| 899 |
+
"solverforge-core",
|
| 900 |
+
"thiserror",
|
| 901 |
+
"toml",
|
| 902 |
+
]
|
| 903 |
+
|
| 904 |
+
[[package]]
|
| 905 |
+
name = "solverforge-core"
|
| 906 |
+
version = "0.5.0"
|
| 907 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 908 |
+
checksum = "f9340b4e2e13d3cf0f2525a6b4e7fbb674819ccacb6cd6ea8777abe8baa463f3"
|
| 909 |
+
dependencies = [
|
| 910 |
+
"num-traits",
|
| 911 |
+
"serde",
|
| 912 |
+
"thiserror",
|
| 913 |
+
]
|
| 914 |
+
|
| 915 |
+
[[package]]
|
| 916 |
+
name = "solverforge-macros"
|
| 917 |
+
version = "0.5.0"
|
| 918 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 919 |
+
checksum = "27b9c7720596cef52b919937b08eb65ec3e341010c249e7f5270e4ab4b879956"
|
| 920 |
+
dependencies = [
|
| 921 |
+
"proc-macro2",
|
| 922 |
+
"quote",
|
| 923 |
+
"syn",
|
| 924 |
+
]
|
| 925 |
+
|
| 926 |
+
[[package]]
|
| 927 |
+
name = "solverforge-scoring"
|
| 928 |
+
version = "0.5.0"
|
| 929 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 930 |
+
checksum = "924e94cad37c0942ffa549cc92c5eb65f2d7567e31c57c32751a5222d23b2523"
|
| 931 |
+
dependencies = [
|
| 932 |
+
"solverforge-core",
|
| 933 |
+
"thiserror",
|
| 934 |
+
]
|
| 935 |
+
|
| 936 |
+
[[package]]
|
| 937 |
+
name = "solverforge-solver"
|
| 938 |
+
version = "0.5.0"
|
| 939 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 940 |
+
checksum = "07696c23f16d5b9f5f0eb5afbacdd2f44daf1fd1d123ba31bbdbf9e5bb8f4b63"
|
| 941 |
+
dependencies = [
|
| 942 |
+
"rand 0.9.2",
|
| 943 |
+
"rand_chacha 0.9.0",
|
| 944 |
+
"rayon",
|
| 945 |
+
"serde",
|
| 946 |
+
"smallvec",
|
| 947 |
+
"solverforge-config",
|
| 948 |
+
"solverforge-core",
|
| 949 |
+
"solverforge-scoring",
|
| 950 |
+
"thiserror",
|
| 951 |
+
"tokio",
|
| 952 |
+
"tracing",
|
| 953 |
+
]
|
| 954 |
+
|
| 955 |
+
[[package]]
|
| 956 |
+
name = "syn"
|
| 957 |
+
version = "2.0.113"
|
| 958 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 959 |
+
checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
|
| 960 |
+
dependencies = [
|
| 961 |
+
"proc-macro2",
|
| 962 |
+
"quote",
|
| 963 |
+
"unicode-ident",
|
| 964 |
+
]
|
| 965 |
+
|
| 966 |
+
[[package]]
|
| 967 |
+
name = "sync_wrapper"
|
| 968 |
+
version = "1.0.2"
|
| 969 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 970 |
+
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
| 971 |
+
|
| 972 |
+
[[package]]
|
| 973 |
+
name = "thiserror"
|
| 974 |
+
version = "2.0.17"
|
| 975 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 976 |
+
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
| 977 |
+
dependencies = [
|
| 978 |
+
"thiserror-impl",
|
| 979 |
+
]
|
| 980 |
+
|
| 981 |
+
[[package]]
|
| 982 |
+
name = "thiserror-impl"
|
| 983 |
+
version = "2.0.17"
|
| 984 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 985 |
+
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
| 986 |
+
dependencies = [
|
| 987 |
+
"proc-macro2",
|
| 988 |
+
"quote",
|
| 989 |
+
"syn",
|
| 990 |
+
]
|
| 991 |
+
|
| 992 |
+
[[package]]
|
| 993 |
+
name = "thread_local"
|
| 994 |
+
version = "1.1.9"
|
| 995 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 996 |
+
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
| 997 |
+
dependencies = [
|
| 998 |
+
"cfg-if",
|
| 999 |
+
]
|
| 1000 |
+
|
| 1001 |
+
[[package]]
|
| 1002 |
+
name = "tokio"
|
| 1003 |
+
version = "1.49.0"
|
| 1004 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1005 |
+
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
| 1006 |
+
dependencies = [
|
| 1007 |
+
"bytes",
|
| 1008 |
+
"libc",
|
| 1009 |
+
"mio",
|
| 1010 |
+
"parking_lot",
|
| 1011 |
+
"pin-project-lite",
|
| 1012 |
+
"signal-hook-registry",
|
| 1013 |
+
"socket2",
|
| 1014 |
+
"tokio-macros",
|
| 1015 |
+
"windows-sys 0.61.2",
|
| 1016 |
+
]
|
| 1017 |
+
|
| 1018 |
+
[[package]]
|
| 1019 |
+
name = "tokio-macros"
|
| 1020 |
+
version = "2.6.0"
|
| 1021 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1022 |
+
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
| 1023 |
+
dependencies = [
|
| 1024 |
+
"proc-macro2",
|
| 1025 |
+
"quote",
|
| 1026 |
+
"syn",
|
| 1027 |
+
]
|
| 1028 |
+
|
| 1029 |
+
[[package]]
|
| 1030 |
+
name = "tokio-util"
|
| 1031 |
+
version = "0.7.18"
|
| 1032 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1033 |
+
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
| 1034 |
+
dependencies = [
|
| 1035 |
+
"bytes",
|
| 1036 |
+
"futures-core",
|
| 1037 |
+
"futures-sink",
|
| 1038 |
+
"pin-project-lite",
|
| 1039 |
+
"tokio",
|
| 1040 |
+
]
|
| 1041 |
+
|
| 1042 |
+
[[package]]
|
| 1043 |
+
name = "toml"
|
| 1044 |
+
version = "0.8.23"
|
| 1045 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1046 |
+
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
| 1047 |
+
dependencies = [
|
| 1048 |
+
"serde",
|
| 1049 |
+
"serde_spanned",
|
| 1050 |
+
"toml_datetime",
|
| 1051 |
+
"toml_edit",
|
| 1052 |
+
]
|
| 1053 |
+
|
| 1054 |
+
[[package]]
|
| 1055 |
+
name = "toml_datetime"
|
| 1056 |
+
version = "0.6.11"
|
| 1057 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1058 |
+
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
| 1059 |
+
dependencies = [
|
| 1060 |
+
"serde",
|
| 1061 |
+
]
|
| 1062 |
+
|
| 1063 |
+
[[package]]
|
| 1064 |
+
name = "toml_edit"
|
| 1065 |
+
version = "0.22.27"
|
| 1066 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1067 |
+
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
| 1068 |
+
dependencies = [
|
| 1069 |
+
"indexmap",
|
| 1070 |
+
"serde",
|
| 1071 |
+
"serde_spanned",
|
| 1072 |
+
"toml_datetime",
|
| 1073 |
+
"toml_write",
|
| 1074 |
+
"winnow",
|
| 1075 |
+
]
|
| 1076 |
+
|
| 1077 |
+
[[package]]
|
| 1078 |
+
name = "toml_write"
|
| 1079 |
+
version = "0.1.2"
|
| 1080 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1081 |
+
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
| 1082 |
+
|
| 1083 |
+
[[package]]
|
| 1084 |
+
name = "tower"
|
| 1085 |
+
version = "0.5.2"
|
| 1086 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1087 |
+
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
| 1088 |
+
dependencies = [
|
| 1089 |
+
"futures-core",
|
| 1090 |
+
"futures-util",
|
| 1091 |
+
"pin-project-lite",
|
| 1092 |
+
"sync_wrapper",
|
| 1093 |
+
"tokio",
|
| 1094 |
+
"tower-layer",
|
| 1095 |
+
"tower-service",
|
| 1096 |
+
"tracing",
|
| 1097 |
+
]
|
| 1098 |
+
|
| 1099 |
+
[[package]]
|
| 1100 |
+
name = "tower-http"
|
| 1101 |
+
version = "0.6.8"
|
| 1102 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1103 |
+
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
| 1104 |
+
dependencies = [
|
| 1105 |
+
"bitflags",
|
| 1106 |
+
"bytes",
|
| 1107 |
+
"futures-core",
|
| 1108 |
+
"futures-util",
|
| 1109 |
+
"http",
|
| 1110 |
+
"http-body",
|
| 1111 |
+
"http-body-util",
|
| 1112 |
+
"http-range-header",
|
| 1113 |
+
"httpdate",
|
| 1114 |
+
"mime",
|
| 1115 |
+
"mime_guess",
|
| 1116 |
+
"percent-encoding",
|
| 1117 |
+
"pin-project-lite",
|
| 1118 |
+
"tokio",
|
| 1119 |
+
"tokio-util",
|
| 1120 |
+
"tower-layer",
|
| 1121 |
+
"tower-service",
|
| 1122 |
+
"tracing",
|
| 1123 |
+
]
|
| 1124 |
+
|
| 1125 |
+
[[package]]
|
| 1126 |
+
name = "tower-layer"
|
| 1127 |
+
version = "0.3.3"
|
| 1128 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1129 |
+
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
| 1130 |
+
|
| 1131 |
+
[[package]]
|
| 1132 |
+
name = "tower-service"
|
| 1133 |
+
version = "0.3.3"
|
| 1134 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1135 |
+
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
| 1136 |
+
|
| 1137 |
+
[[package]]
|
| 1138 |
+
name = "tracing"
|
| 1139 |
+
version = "0.1.44"
|
| 1140 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1141 |
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
| 1142 |
+
dependencies = [
|
| 1143 |
+
"log",
|
| 1144 |
+
"pin-project-lite",
|
| 1145 |
+
"tracing-attributes",
|
| 1146 |
+
"tracing-core",
|
| 1147 |
+
]
|
| 1148 |
+
|
| 1149 |
+
[[package]]
|
| 1150 |
+
name = "tracing-attributes"
|
| 1151 |
+
version = "0.1.31"
|
| 1152 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1153 |
+
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
| 1154 |
+
dependencies = [
|
| 1155 |
+
"proc-macro2",
|
| 1156 |
+
"quote",
|
| 1157 |
+
"syn",
|
| 1158 |
+
]
|
| 1159 |
+
|
| 1160 |
+
[[package]]
|
| 1161 |
+
name = "tracing-core"
|
| 1162 |
+
version = "0.1.36"
|
| 1163 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1164 |
+
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
| 1165 |
+
dependencies = [
|
| 1166 |
+
"once_cell",
|
| 1167 |
+
"valuable",
|
| 1168 |
+
]
|
| 1169 |
+
|
| 1170 |
+
[[package]]
|
| 1171 |
+
name = "tracing-log"
|
| 1172 |
+
version = "0.2.0"
|
| 1173 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1174 |
+
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
| 1175 |
+
dependencies = [
|
| 1176 |
+
"log",
|
| 1177 |
+
"once_cell",
|
| 1178 |
+
"tracing-core",
|
| 1179 |
+
]
|
| 1180 |
+
|
| 1181 |
+
[[package]]
|
| 1182 |
+
name = "tracing-subscriber"
|
| 1183 |
+
version = "0.3.22"
|
| 1184 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1185 |
+
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
| 1186 |
+
dependencies = [
|
| 1187 |
+
"matchers",
|
| 1188 |
+
"nu-ansi-term",
|
| 1189 |
+
"once_cell",
|
| 1190 |
+
"regex-automata",
|
| 1191 |
+
"sharded-slab",
|
| 1192 |
+
"smallvec",
|
| 1193 |
+
"thread_local",
|
| 1194 |
+
"tracing",
|
| 1195 |
+
"tracing-core",
|
| 1196 |
+
"tracing-log",
|
| 1197 |
+
]
|
| 1198 |
+
|
| 1199 |
+
[[package]]
|
| 1200 |
+
name = "unicase"
|
| 1201 |
+
version = "2.8.1"
|
| 1202 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1203 |
+
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
| 1204 |
+
|
| 1205 |
+
[[package]]
|
| 1206 |
+
name = "unicode-ident"
|
| 1207 |
+
version = "1.0.22"
|
| 1208 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1209 |
+
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
| 1210 |
+
|
| 1211 |
+
[[package]]
|
| 1212 |
+
name = "unsafe-libyaml"
|
| 1213 |
+
version = "0.2.11"
|
| 1214 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1215 |
+
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
| 1216 |
+
|
| 1217 |
+
[[package]]
|
| 1218 |
+
name = "uuid"
|
| 1219 |
+
version = "1.19.0"
|
| 1220 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1221 |
+
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
| 1222 |
+
dependencies = [
|
| 1223 |
+
"getrandom 0.3.4",
|
| 1224 |
+
"js-sys",
|
| 1225 |
+
"serde_core",
|
| 1226 |
+
"wasm-bindgen",
|
| 1227 |
+
]
|
| 1228 |
+
|
| 1229 |
+
[[package]]
|
| 1230 |
+
name = "valuable"
|
| 1231 |
+
version = "0.1.1"
|
| 1232 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1233 |
+
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
| 1234 |
+
|
| 1235 |
+
[[package]]
|
| 1236 |
+
name = "wasi"
|
| 1237 |
+
version = "0.11.1+wasi-snapshot-preview1"
|
| 1238 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1239 |
+
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
| 1240 |
+
|
| 1241 |
+
[[package]]
|
| 1242 |
+
name = "wasip2"
|
| 1243 |
+
version = "1.0.1+wasi-0.2.4"
|
| 1244 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1245 |
+
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
| 1246 |
+
dependencies = [
|
| 1247 |
+
"wit-bindgen",
|
| 1248 |
+
]
|
| 1249 |
+
|
| 1250 |
+
[[package]]
|
| 1251 |
+
name = "wasm-bindgen"
|
| 1252 |
+
version = "0.2.106"
|
| 1253 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1254 |
+
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
| 1255 |
+
dependencies = [
|
| 1256 |
+
"cfg-if",
|
| 1257 |
+
"once_cell",
|
| 1258 |
+
"rustversion",
|
| 1259 |
+
"wasm-bindgen-macro",
|
| 1260 |
+
"wasm-bindgen-shared",
|
| 1261 |
+
]
|
| 1262 |
+
|
| 1263 |
+
[[package]]
|
| 1264 |
+
name = "wasm-bindgen-macro"
|
| 1265 |
+
version = "0.2.106"
|
| 1266 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1267 |
+
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
| 1268 |
+
dependencies = [
|
| 1269 |
+
"quote",
|
| 1270 |
+
"wasm-bindgen-macro-support",
|
| 1271 |
+
]
|
| 1272 |
+
|
| 1273 |
+
[[package]]
|
| 1274 |
+
name = "wasm-bindgen-macro-support"
|
| 1275 |
+
version = "0.2.106"
|
| 1276 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1277 |
+
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
| 1278 |
+
dependencies = [
|
| 1279 |
+
"bumpalo",
|
| 1280 |
+
"proc-macro2",
|
| 1281 |
+
"quote",
|
| 1282 |
+
"syn",
|
| 1283 |
+
"wasm-bindgen-shared",
|
| 1284 |
+
]
|
| 1285 |
+
|
| 1286 |
+
[[package]]
|
| 1287 |
+
name = "wasm-bindgen-shared"
|
| 1288 |
+
version = "0.2.106"
|
| 1289 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1290 |
+
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
| 1291 |
+
dependencies = [
|
| 1292 |
+
"unicode-ident",
|
| 1293 |
+
]
|
| 1294 |
+
|
| 1295 |
+
[[package]]
|
| 1296 |
+
name = "windows-core"
|
| 1297 |
+
version = "0.62.2"
|
| 1298 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1299 |
+
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
| 1300 |
+
dependencies = [
|
| 1301 |
+
"windows-implement",
|
| 1302 |
+
"windows-interface",
|
| 1303 |
+
"windows-link",
|
| 1304 |
+
"windows-result",
|
| 1305 |
+
"windows-strings",
|
| 1306 |
+
]
|
| 1307 |
+
|
| 1308 |
+
[[package]]
|
| 1309 |
+
name = "windows-implement"
|
| 1310 |
+
version = "0.60.2"
|
| 1311 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1312 |
+
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
| 1313 |
+
dependencies = [
|
| 1314 |
+
"proc-macro2",
|
| 1315 |
+
"quote",
|
| 1316 |
+
"syn",
|
| 1317 |
+
]
|
| 1318 |
+
|
| 1319 |
+
[[package]]
|
| 1320 |
+
name = "windows-interface"
|
| 1321 |
+
version = "0.59.3"
|
| 1322 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1323 |
+
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
| 1324 |
+
dependencies = [
|
| 1325 |
+
"proc-macro2",
|
| 1326 |
+
"quote",
|
| 1327 |
+
"syn",
|
| 1328 |
+
]
|
| 1329 |
+
|
| 1330 |
+
[[package]]
|
| 1331 |
+
name = "windows-link"
|
| 1332 |
+
version = "0.2.1"
|
| 1333 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1334 |
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
| 1335 |
+
|
| 1336 |
+
[[package]]
|
| 1337 |
+
name = "windows-result"
|
| 1338 |
+
version = "0.4.1"
|
| 1339 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1340 |
+
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
| 1341 |
+
dependencies = [
|
| 1342 |
+
"windows-link",
|
| 1343 |
+
]
|
| 1344 |
+
|
| 1345 |
+
[[package]]
|
| 1346 |
+
name = "windows-strings"
|
| 1347 |
+
version = "0.5.1"
|
| 1348 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1349 |
+
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
| 1350 |
+
dependencies = [
|
| 1351 |
+
"windows-link",
|
| 1352 |
+
]
|
| 1353 |
+
|
| 1354 |
+
[[package]]
|
| 1355 |
+
name = "windows-sys"
|
| 1356 |
+
version = "0.60.2"
|
| 1357 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1358 |
+
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
| 1359 |
+
dependencies = [
|
| 1360 |
+
"windows-targets",
|
| 1361 |
+
]
|
| 1362 |
+
|
| 1363 |
+
[[package]]
|
| 1364 |
+
name = "windows-sys"
|
| 1365 |
+
version = "0.61.2"
|
| 1366 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1367 |
+
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
| 1368 |
+
dependencies = [
|
| 1369 |
+
"windows-link",
|
| 1370 |
+
]
|
| 1371 |
+
|
| 1372 |
+
[[package]]
|
| 1373 |
+
name = "windows-targets"
|
| 1374 |
+
version = "0.53.5"
|
| 1375 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1376 |
+
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
| 1377 |
+
dependencies = [
|
| 1378 |
+
"windows-link",
|
| 1379 |
+
"windows_aarch64_gnullvm",
|
| 1380 |
+
"windows_aarch64_msvc",
|
| 1381 |
+
"windows_i686_gnu",
|
| 1382 |
+
"windows_i686_gnullvm",
|
| 1383 |
+
"windows_i686_msvc",
|
| 1384 |
+
"windows_x86_64_gnu",
|
| 1385 |
+
"windows_x86_64_gnullvm",
|
| 1386 |
+
"windows_x86_64_msvc",
|
| 1387 |
+
]
|
| 1388 |
+
|
| 1389 |
+
[[package]]
|
| 1390 |
+
name = "windows_aarch64_gnullvm"
|
| 1391 |
+
version = "0.53.1"
|
| 1392 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1393 |
+
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
| 1394 |
+
|
| 1395 |
+
[[package]]
|
| 1396 |
+
name = "windows_aarch64_msvc"
|
| 1397 |
+
version = "0.53.1"
|
| 1398 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1399 |
+
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
| 1400 |
+
|
| 1401 |
+
[[package]]
|
| 1402 |
+
name = "windows_i686_gnu"
|
| 1403 |
+
version = "0.53.1"
|
| 1404 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1405 |
+
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
| 1406 |
+
|
| 1407 |
+
[[package]]
|
| 1408 |
+
name = "windows_i686_gnullvm"
|
| 1409 |
+
version = "0.53.1"
|
| 1410 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1411 |
+
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
| 1412 |
+
|
| 1413 |
+
[[package]]
|
| 1414 |
+
name = "windows_i686_msvc"
|
| 1415 |
+
version = "0.53.1"
|
| 1416 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1417 |
+
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
| 1418 |
+
|
| 1419 |
+
[[package]]
|
| 1420 |
+
name = "windows_x86_64_gnu"
|
| 1421 |
+
version = "0.53.1"
|
| 1422 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1423 |
+
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
| 1424 |
+
|
| 1425 |
+
[[package]]
|
| 1426 |
+
name = "windows_x86_64_gnullvm"
|
| 1427 |
+
version = "0.53.1"
|
| 1428 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1429 |
+
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
| 1430 |
+
|
| 1431 |
+
[[package]]
|
| 1432 |
+
name = "windows_x86_64_msvc"
|
| 1433 |
+
version = "0.53.1"
|
| 1434 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1435 |
+
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
| 1436 |
+
|
| 1437 |
+
[[package]]
|
| 1438 |
+
name = "winnow"
|
| 1439 |
+
version = "0.7.14"
|
| 1440 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1441 |
+
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
| 1442 |
+
dependencies = [
|
| 1443 |
+
"memchr",
|
| 1444 |
+
]
|
| 1445 |
+
|
| 1446 |
+
[[package]]
|
| 1447 |
+
name = "wit-bindgen"
|
| 1448 |
+
version = "0.46.0"
|
| 1449 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1450 |
+
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
| 1451 |
+
|
| 1452 |
+
[[package]]
|
| 1453 |
+
name = "zerocopy"
|
| 1454 |
+
version = "0.8.31"
|
| 1455 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1456 |
+
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
|
| 1457 |
+
dependencies = [
|
| 1458 |
+
"zerocopy-derive",
|
| 1459 |
+
]
|
| 1460 |
+
|
| 1461 |
+
[[package]]
|
| 1462 |
+
name = "zerocopy-derive"
|
| 1463 |
+
version = "0.8.31"
|
| 1464 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1465 |
+
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
|
| 1466 |
+
dependencies = [
|
| 1467 |
+
"proc-macro2",
|
| 1468 |
+
"quote",
|
| 1469 |
+
"syn",
|
| 1470 |
+
]
|
| 1471 |
+
|
| 1472 |
+
[[package]]
|
| 1473 |
+
name = "zmij"
|
| 1474 |
+
version = "1.0.10"
|
| 1475 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1476 |
+
checksum = "30e0d8dffbae3d840f64bda38e28391faef673a7b5a6017840f2a106c8145868"
|
Cargo.toml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[package]
|
| 2 |
+
name = "employee-scheduling"
|
| 3 |
+
version = "0.5.0"
|
| 4 |
+
edition = "2021"
|
| 5 |
+
description = "Employee scheduling quickstart for SolverForge"
|
| 6 |
+
publish = false
|
| 7 |
+
|
| 8 |
+
[dependencies]
|
| 9 |
+
solverforge = { version = "0.5.0", features = ["serde", "console", "verbose-logging"] }
|
| 10 |
+
rayon = "1"
|
| 11 |
+
rand = "0.8"
|
| 12 |
+
|
| 13 |
+
axum = "0.8"
|
| 14 |
+
tokio = { version = "1", features = ["full"] }
|
| 15 |
+
tower-http = { version = "0.6", features = ["fs", "cors"] }
|
| 16 |
+
tower = "0.5"
|
| 17 |
+
serde = { version = "1", features = ["derive"] }
|
| 18 |
+
serde_json = "1"
|
| 19 |
+
chrono = { version = "0.4", features = ["serde"] }
|
| 20 |
+
uuid = { version = "1", features = ["v4", "serde"] }
|
| 21 |
+
parking_lot = "0.12"
|
Dockerfile
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multi-stage build for SolverForge Employee Scheduling (Rust)
|
| 2 |
+
#
|
| 3 |
+
# Build context: rust/employee-scheduling/
|
| 4 |
+
# Uses published solverforge crate from crates.io
|
| 5 |
+
|
| 6 |
+
FROM rust:1.83-alpine AS builder
|
| 7 |
+
|
| 8 |
+
# Install build dependencies
|
| 9 |
+
RUN apk add --no-cache musl-dev
|
| 10 |
+
|
| 11 |
+
WORKDIR /build
|
| 12 |
+
|
| 13 |
+
# Copy workspace files
|
| 14 |
+
COPY Cargo.toml Cargo.lock ./
|
| 15 |
+
COPY src/ ./src/
|
| 16 |
+
COPY static/ ./static/
|
| 17 |
+
COPY solver.toml ./
|
| 18 |
+
|
| 19 |
+
# Build release binary with musl target for static linking
|
| 20 |
+
RUN cargo build --release --target x86_64-unknown-linux-musl
|
| 21 |
+
|
| 22 |
+
# Runtime stage - minimal Alpine image
|
| 23 |
+
FROM alpine:latest
|
| 24 |
+
|
| 25 |
+
RUN apk add --no-cache ca-certificates
|
| 26 |
+
|
| 27 |
+
WORKDIR /app
|
| 28 |
+
|
| 29 |
+
# Copy binary from builder (musl static binary)
|
| 30 |
+
COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/employee-scheduling ./employee-scheduling
|
| 31 |
+
|
| 32 |
+
# Copy static files
|
| 33 |
+
COPY --from=builder /build/static/ ./static/
|
| 34 |
+
|
| 35 |
+
# Copy solver config
|
| 36 |
+
COPY --from=builder /build/solver.toml ./solver.toml
|
| 37 |
+
|
| 38 |
+
# Expose port 7860 (HF Spaces default)
|
| 39 |
+
EXPOSE 7860
|
| 40 |
+
|
| 41 |
+
# Run the application
|
| 42 |
+
CMD ["./employee-scheduling"]
|
README.md
CHANGED
|
@@ -1,12 +1,97 @@
|
|
| 1 |
---
|
| 2 |
-
title: Employee Scheduling Rust
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: apache-2.0
|
| 9 |
-
short_description: SolverForge Quickstart for
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Employee Scheduling (Rust)
|
| 3 |
+
emoji: 📅
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: red
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
license: apache-2.0
|
| 10 |
+
short_description: SolverForge Quickstart for Employee Scheduling in Rust
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Employee Scheduling (Rust)
|
| 14 |
+
|
| 15 |
+
Schedule shifts to employees, accounting for employee availability and shift skill requirements.
|
| 16 |
+
|
| 17 |
+
- [Prerequisites](#prerequisites)
|
| 18 |
+
- [Run the application](#run-the-application)
|
| 19 |
+
- [Test the application](#test-the-application)
|
| 20 |
+
- [REST API](#rest-api)
|
| 21 |
+
- [Constraints](#constraints)
|
| 22 |
+
- [More information](#more-information)
|
| 23 |
+
|
| 24 |
+
## Prerequisites
|
| 25 |
+
|
| 26 |
+
1. Install [Rust](https://www.rust-lang.org/tools/install) (1.70 or later):
|
| 27 |
+
|
| 28 |
+
```sh
|
| 29 |
+
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## Run the application
|
| 33 |
+
|
| 34 |
+
1. Git clone the solverforge-quickstarts repo and navigate to this directory:
|
| 35 |
+
|
| 36 |
+
```sh
|
| 37 |
+
$ git clone https://github.com/SolverForge/solverforge-quickstarts.git
|
| 38 |
+
...
|
| 39 |
+
$ cd solverforge-quickstarts/rust/employee-scheduling
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
2. Build and run the application:
|
| 43 |
+
|
| 44 |
+
```sh
|
| 45 |
+
$ cargo run --release
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
3. Visit [http://localhost:7860](http://localhost:7860) in your browser.
|
| 49 |
+
|
| 50 |
+
4. Click on the **Solve** button.
|
| 51 |
+
|
| 52 |
+
## Test the application
|
| 53 |
+
|
| 54 |
+
1. Run tests:
|
| 55 |
+
|
| 56 |
+
```sh
|
| 57 |
+
$ cargo test
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
## Docker
|
| 61 |
+
|
| 62 |
+
You can also run the application using Docker:
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
# From repository root
|
| 66 |
+
$ docker build -f rust/employee-scheduling/Dockerfile -t employee-scheduling-rust .
|
| 67 |
+
$ docker run -p 7860:7860 employee-scheduling-rust
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Then visit [http://localhost:7860](http://localhost:7860) in your browser.
|
| 71 |
+
|
| 72 |
+
## REST API
|
| 73 |
+
|
| 74 |
+
- `GET /demo-data` - List available demo datasets
|
| 75 |
+
- `GET /demo-data/{id}` - Get specific demo data
|
| 76 |
+
- `POST /schedules` - Start solving (returns job ID)
|
| 77 |
+
- `GET /schedules/{id}` - Get current solution
|
| 78 |
+
- `DELETE /schedules/{id}` - Stop solving
|
| 79 |
+
- `PUT /schedules/analyze` - Analyze constraint violations
|
| 80 |
+
|
| 81 |
+
## Constraints
|
| 82 |
+
|
| 83 |
+
**Hard Constraints** (must be satisfied):
|
| 84 |
+
- Required skill match
|
| 85 |
+
- No overlapping shifts
|
| 86 |
+
- Minimum 10 hours between shifts
|
| 87 |
+
- One shift per day per employee
|
| 88 |
+
- Respect unavailable dates
|
| 89 |
+
|
| 90 |
+
**Soft Constraints** (optimized):
|
| 91 |
+
- Avoid undesired dates
|
| 92 |
+
- Prefer desired dates
|
| 93 |
+
- Balance shift assignments across employees
|
| 94 |
+
|
| 95 |
+
## More information
|
| 96 |
+
|
| 97 |
+
Visit [solverforge.org](https://www.solverforge.org).
|
helm/employee-scheduling/.helmignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
.git/
|
| 3 |
+
.gitignore
|
| 4 |
+
.bzr/
|
| 5 |
+
.bzrignore
|
| 6 |
+
.hg/
|
| 7 |
+
.hgignore
|
| 8 |
+
.svn/
|
| 9 |
+
*.swp
|
| 10 |
+
*.bak
|
| 11 |
+
*.tmp
|
| 12 |
+
*.orig
|
| 13 |
+
*~
|
| 14 |
+
.project
|
| 15 |
+
.idea/
|
| 16 |
+
*.tmproj
|
| 17 |
+
.vscode/
|
helm/employee-scheduling/Chart.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
apiVersion: v2
|
| 2 |
+
name: employee-scheduling
|
| 3 |
+
description: Employee Scheduling optimization using SolverForge (Rust/Axum)
|
| 4 |
+
type: application
|
| 5 |
+
version: 0.5.0
|
| 6 |
+
appVersion: "0.5.0"
|
| 7 |
+
keywords:
|
| 8 |
+
- solverforge
|
| 9 |
+
- optimization
|
| 10 |
+
- scheduling
|
| 11 |
+
- rust
|
| 12 |
+
- axum
|
helm/employee-scheduling/templates/NOTES.txt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Thank you for installing {{ .Chart.Name }}.
|
| 2 |
+
|
| 3 |
+
Your release is named {{ .Release.Name }}.
|
| 4 |
+
|
| 5 |
+
To learn more about the release, try:
|
| 6 |
+
|
| 7 |
+
$ helm status {{ .Release.Name }}
|
| 8 |
+
$ helm get all {{ .Release.Name }}
|
| 9 |
+
|
| 10 |
+
{{- if .Values.ingress.enabled }}
|
| 11 |
+
|
| 12 |
+
The application is accessible via:
|
| 13 |
+
{{- range $host := .Values.ingress.hosts }}
|
| 14 |
+
{{- range .paths }}
|
| 15 |
+
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
| 16 |
+
{{- end }}
|
| 17 |
+
{{- end }}
|
| 18 |
+
|
| 19 |
+
{{- else }}
|
| 20 |
+
|
| 21 |
+
To access the application, run:
|
| 22 |
+
|
| 23 |
+
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
| 24 |
+
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
| 25 |
+
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
| 26 |
+
|
| 27 |
+
Then open http://localhost:8080 in your browser.
|
| 28 |
+
|
| 29 |
+
{{- end }}
|
| 30 |
+
|
| 31 |
+
Demo Data: http://localhost:8080/demo-data
|
helm/employee-scheduling/templates/_helpers.tpl
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{{/*
|
| 2 |
+
Expand the name of the chart.
|
| 3 |
+
*/}}
|
| 4 |
+
{{- define "app.name" -}}
|
| 5 |
+
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
| 6 |
+
{{- end }}
|
| 7 |
+
|
| 8 |
+
{{/*
|
| 9 |
+
Create a default fully qualified app name.
|
| 10 |
+
*/}}
|
| 11 |
+
{{- define "app.fullname" -}}
|
| 12 |
+
{{- if .Values.fullnameOverride }}
|
| 13 |
+
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
| 14 |
+
{{- else }}
|
| 15 |
+
{{- $name := default .Chart.Name .Values.nameOverride }}
|
| 16 |
+
{{- if contains $name .Release.Name }}
|
| 17 |
+
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
| 18 |
+
{{- else }}
|
| 19 |
+
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
| 20 |
+
{{- end }}
|
| 21 |
+
{{- end }}
|
| 22 |
+
{{- end }}
|
| 23 |
+
|
| 24 |
+
{{/*
|
| 25 |
+
Create chart name and version as used by the chart label.
|
| 26 |
+
*/}}
|
| 27 |
+
{{- define "app.chart" -}}
|
| 28 |
+
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
| 29 |
+
{{- end }}
|
| 30 |
+
|
| 31 |
+
{{/*
|
| 32 |
+
Common labels
|
| 33 |
+
*/}}
|
| 34 |
+
{{- define "app.labels" -}}
|
| 35 |
+
helm.sh/chart: {{ include "app.chart" . }}
|
| 36 |
+
{{ include "app.selectorLabels" . }}
|
| 37 |
+
{{- if .Chart.AppVersion }}
|
| 38 |
+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
| 39 |
+
{{- end }}
|
| 40 |
+
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
| 41 |
+
{{- end }}
|
| 42 |
+
|
| 43 |
+
{{/*
|
| 44 |
+
Selector labels
|
| 45 |
+
*/}}
|
| 46 |
+
{{- define "app.selectorLabels" -}}
|
| 47 |
+
app.kubernetes.io/name: {{ include "app.name" . }}
|
| 48 |
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
| 49 |
+
{{- end }}
|
helm/employee-scheduling/templates/deployment.yaml
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
apiVersion: apps/v1
|
| 2 |
+
kind: Deployment
|
| 3 |
+
metadata:
|
| 4 |
+
name: {{ include "app.fullname" . }}
|
| 5 |
+
labels:
|
| 6 |
+
{{- include "app.labels" . | nindent 4 }}
|
| 7 |
+
spec:
|
| 8 |
+
{{- if not .Values.autoscaling.enabled }}
|
| 9 |
+
replicas: {{ .Values.replicaCount }}
|
| 10 |
+
{{- end }}
|
| 11 |
+
selector:
|
| 12 |
+
matchLabels:
|
| 13 |
+
{{- include "app.selectorLabels" . | nindent 6 }}
|
| 14 |
+
template:
|
| 15 |
+
metadata:
|
| 16 |
+
{{- with .Values.podAnnotations }}
|
| 17 |
+
annotations:
|
| 18 |
+
{{- toYaml . | nindent 8 }}
|
| 19 |
+
{{- end }}
|
| 20 |
+
labels:
|
| 21 |
+
{{- include "app.labels" . | nindent 8 }}
|
| 22 |
+
{{- with .Values.podLabels }}
|
| 23 |
+
{{- toYaml . | nindent 8 }}
|
| 24 |
+
{{- end }}
|
| 25 |
+
spec:
|
| 26 |
+
{{- with .Values.imagePullSecrets }}
|
| 27 |
+
imagePullSecrets:
|
| 28 |
+
{{- toYaml . | nindent 8 }}
|
| 29 |
+
{{- end }}
|
| 30 |
+
securityContext:
|
| 31 |
+
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
| 32 |
+
containers:
|
| 33 |
+
- name: {{ .Chart.Name }}
|
| 34 |
+
securityContext:
|
| 35 |
+
{{- toYaml .Values.securityContext | nindent 12 }}
|
| 36 |
+
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
| 37 |
+
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
| 38 |
+
ports:
|
| 39 |
+
- name: http
|
| 40 |
+
containerPort: 8080
|
| 41 |
+
protocol: TCP
|
| 42 |
+
{{- with .Values.livenessProbe }}
|
| 43 |
+
livenessProbe:
|
| 44 |
+
{{- toYaml . | nindent 12 }}
|
| 45 |
+
{{- end }}
|
| 46 |
+
{{- with .Values.readinessProbe }}
|
| 47 |
+
readinessProbe:
|
| 48 |
+
{{- toYaml . | nindent 12 }}
|
| 49 |
+
{{- end }}
|
| 50 |
+
resources:
|
| 51 |
+
{{- toYaml .Values.resources | nindent 12 }}
|
| 52 |
+
{{- with .Values.env }}
|
| 53 |
+
env:
|
| 54 |
+
{{- toYaml . | nindent 12 }}
|
| 55 |
+
{{- end }}
|
| 56 |
+
{{- with .Values.nodeSelector }}
|
| 57 |
+
nodeSelector:
|
| 58 |
+
{{- toYaml . | nindent 8 }}
|
| 59 |
+
{{- end }}
|
| 60 |
+
{{- with .Values.affinity }}
|
| 61 |
+
affinity:
|
| 62 |
+
{{- toYaml . | nindent 8 }}
|
| 63 |
+
{{- end }}
|
| 64 |
+
{{- with .Values.tolerations }}
|
| 65 |
+
tolerations:
|
| 66 |
+
{{- toYaml . | nindent 8 }}
|
| 67 |
+
{{- end }}
|
helm/employee-scheduling/templates/ingress.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{{- if .Values.ingress.enabled -}}
|
| 2 |
+
apiVersion: networking.k8s.io/v1
|
| 3 |
+
kind: Ingress
|
| 4 |
+
metadata:
|
| 5 |
+
name: {{ include "app.fullname" . }}
|
| 6 |
+
labels:
|
| 7 |
+
{{- include "app.labels" . | nindent 4 }}
|
| 8 |
+
{{- with .Values.ingress.annotations }}
|
| 9 |
+
annotations:
|
| 10 |
+
{{- toYaml . | nindent 4 }}
|
| 11 |
+
{{- end }}
|
| 12 |
+
spec:
|
| 13 |
+
{{- if .Values.ingress.className }}
|
| 14 |
+
ingressClassName: {{ .Values.ingress.className }}
|
| 15 |
+
{{- end }}
|
| 16 |
+
{{- if .Values.ingress.tls }}
|
| 17 |
+
tls:
|
| 18 |
+
{{- range .Values.ingress.tls }}
|
| 19 |
+
- hosts:
|
| 20 |
+
{{- range .hosts }}
|
| 21 |
+
- {{ . | quote }}
|
| 22 |
+
{{- end }}
|
| 23 |
+
secretName: {{ .secretName }}
|
| 24 |
+
{{- end }}
|
| 25 |
+
{{- end }}
|
| 26 |
+
rules:
|
| 27 |
+
{{- range .Values.ingress.hosts }}
|
| 28 |
+
- host: {{ .host | quote }}
|
| 29 |
+
http:
|
| 30 |
+
paths:
|
| 31 |
+
{{- range .paths }}
|
| 32 |
+
- path: {{ .path }}
|
| 33 |
+
pathType: {{ .pathType }}
|
| 34 |
+
backend:
|
| 35 |
+
service:
|
| 36 |
+
name: {{ include "app.fullname" $ }}
|
| 37 |
+
port:
|
| 38 |
+
number: {{ $.Values.service.port }}
|
| 39 |
+
{{- end }}
|
| 40 |
+
{{- end }}
|
| 41 |
+
{{- end }}
|
helm/employee-scheduling/templates/service.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
apiVersion: v1
|
| 2 |
+
kind: Service
|
| 3 |
+
metadata:
|
| 4 |
+
name: {{ include "app.fullname" . }}
|
| 5 |
+
labels:
|
| 6 |
+
{{- include "app.labels" . | nindent 4 }}
|
| 7 |
+
spec:
|
| 8 |
+
type: {{ .Values.service.type }}
|
| 9 |
+
ports:
|
| 10 |
+
- port: {{ .Values.service.port }}
|
| 11 |
+
targetPort: http
|
| 12 |
+
protocol: TCP
|
| 13 |
+
name: http
|
| 14 |
+
selector:
|
| 15 |
+
{{- include "app.selectorLabels" . | nindent 4 }}
|
helm/employee-scheduling/values.yaml
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
replicaCount: 1
|
| 2 |
+
|
| 3 |
+
image:
|
| 4 |
+
repository: ghcr.io/solverforge/employee-scheduling
|
| 5 |
+
pullPolicy: IfNotPresent
|
| 6 |
+
tag: "latest"
|
| 7 |
+
|
| 8 |
+
imagePullSecrets: []
|
| 9 |
+
nameOverride: ""
|
| 10 |
+
fullnameOverride: ""
|
| 11 |
+
|
| 12 |
+
podAnnotations: {}
|
| 13 |
+
podLabels: {}
|
| 14 |
+
|
| 15 |
+
podSecurityContext: {}
|
| 16 |
+
|
| 17 |
+
securityContext: {}
|
| 18 |
+
|
| 19 |
+
service:
|
| 20 |
+
type: ClusterIP
|
| 21 |
+
port: 8080
|
| 22 |
+
|
| 23 |
+
ingress:
|
| 24 |
+
enabled: false
|
| 25 |
+
className: ""
|
| 26 |
+
annotations: {}
|
| 27 |
+
hosts:
|
| 28 |
+
- host: employee-scheduling.local
|
| 29 |
+
paths:
|
| 30 |
+
- path: /
|
| 31 |
+
pathType: ImplementationSpecific
|
| 32 |
+
tls: []
|
| 33 |
+
|
| 34 |
+
resources:
|
| 35 |
+
limits:
|
| 36 |
+
cpu: 2000m
|
| 37 |
+
memory: 2Gi
|
| 38 |
+
requests:
|
| 39 |
+
cpu: 250m
|
| 40 |
+
memory: 256Mi
|
| 41 |
+
|
| 42 |
+
livenessProbe:
|
| 43 |
+
httpGet:
|
| 44 |
+
path: /demo-data
|
| 45 |
+
port: http
|
| 46 |
+
initialDelaySeconds: 10
|
| 47 |
+
periodSeconds: 10
|
| 48 |
+
timeoutSeconds: 5
|
| 49 |
+
failureThreshold: 3
|
| 50 |
+
|
| 51 |
+
readinessProbe:
|
| 52 |
+
httpGet:
|
| 53 |
+
path: /demo-data
|
| 54 |
+
port: http
|
| 55 |
+
initialDelaySeconds: 5
|
| 56 |
+
periodSeconds: 5
|
| 57 |
+
timeoutSeconds: 3
|
| 58 |
+
failureThreshold: 3
|
| 59 |
+
|
| 60 |
+
autoscaling:
|
| 61 |
+
enabled: false
|
| 62 |
+
minReplicas: 1
|
| 63 |
+
maxReplicas: 3
|
| 64 |
+
targetCPUUtilizationPercentage: 80
|
| 65 |
+
|
| 66 |
+
nodeSelector: {}
|
| 67 |
+
|
| 68 |
+
tolerations: []
|
| 69 |
+
|
| 70 |
+
affinity: {}
|
| 71 |
+
|
| 72 |
+
env: []
|
solver.toml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SolverForge Configuration for Employee Scheduling
|
| 2 |
+
|
| 3 |
+
[termination]
|
| 4 |
+
seconds_spent_limit = 30
|
| 5 |
+
unimproved_seconds_spent_limit = 5
|
src/api.rs
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! REST API handlers for Employee Scheduling.
|
| 2 |
+
|
| 3 |
+
use axum::{
|
| 4 |
+
extract::{Path, State},
|
| 5 |
+
http::StatusCode,
|
| 6 |
+
routing::{delete, get, post, put},
|
| 7 |
+
Json, Router,
|
| 8 |
+
};
|
| 9 |
+
use chrono::{NaiveDate, NaiveDateTime};
|
| 10 |
+
use parking_lot::RwLock;
|
| 11 |
+
use serde::{Deserialize, Serialize};
|
| 12 |
+
use std::collections::{HashMap, HashSet};
|
| 13 |
+
use std::sync::Arc;
|
| 14 |
+
use uuid::Uuid;
|
| 15 |
+
|
| 16 |
+
use crate::demo_data::{self, DemoData};
|
| 17 |
+
use crate::domain::{Employee, EmployeeSchedule, Shift};
|
| 18 |
+
|
| 19 |
+
/// Job tracking for active solves.
|
| 20 |
+
struct SolveJob {
|
| 21 |
+
solution: EmployeeSchedule,
|
| 22 |
+
solver_status: String,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/// Application state shared across handlers.
|
| 26 |
+
pub struct AppState {
|
| 27 |
+
jobs: RwLock<HashMap<String, SolveJob>>,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
impl AppState {
|
| 31 |
+
pub fn new() -> Self {
|
| 32 |
+
Self {
|
| 33 |
+
jobs: RwLock::new(HashMap::new()),
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
impl Default for AppState {
|
| 39 |
+
fn default() -> Self {
|
| 40 |
+
Self::new()
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// ============================================================================
|
| 45 |
+
// DTOs
|
| 46 |
+
// ============================================================================
|
| 47 |
+
|
| 48 |
+
/// Employee DTO for API requests/responses.
|
| 49 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 50 |
+
#[serde(rename_all = "camelCase")]
|
| 51 |
+
pub struct EmployeeDto {
|
| 52 |
+
pub name: String,
|
| 53 |
+
pub skills: Vec<String>,
|
| 54 |
+
#[serde(default)]
|
| 55 |
+
pub unavailable_dates: Vec<NaiveDate>,
|
| 56 |
+
#[serde(default)]
|
| 57 |
+
pub undesired_dates: Vec<NaiveDate>,
|
| 58 |
+
#[serde(default)]
|
| 59 |
+
pub desired_dates: Vec<NaiveDate>,
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
impl From<&Employee> for EmployeeDto {
|
| 63 |
+
fn from(e: &Employee) -> Self {
|
| 64 |
+
Self {
|
| 65 |
+
name: e.name.clone(),
|
| 66 |
+
skills: e.skills.iter().cloned().collect(),
|
| 67 |
+
unavailable_dates: e.unavailable_dates.iter().cloned().collect(),
|
| 68 |
+
undesired_dates: e.undesired_dates.iter().cloned().collect(),
|
| 69 |
+
desired_dates: e.desired_dates.iter().cloned().collect(),
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
impl EmployeeDto {
|
| 75 |
+
fn to_employee(&self, index: usize) -> Employee {
|
| 76 |
+
let unavailable_dates: HashSet<NaiveDate> =
|
| 77 |
+
self.unavailable_dates.iter().cloned().collect();
|
| 78 |
+
let undesired_dates: HashSet<NaiveDate> =
|
| 79 |
+
self.undesired_dates.iter().cloned().collect();
|
| 80 |
+
let desired_dates: HashSet<NaiveDate> =
|
| 81 |
+
self.desired_dates.iter().cloned().collect();
|
| 82 |
+
|
| 83 |
+
let mut unavailable_days: Vec<NaiveDate> = unavailable_dates.iter().copied().collect();
|
| 84 |
+
unavailable_days.sort();
|
| 85 |
+
let mut undesired_days: Vec<NaiveDate> = undesired_dates.iter().copied().collect();
|
| 86 |
+
undesired_days.sort();
|
| 87 |
+
let mut desired_days: Vec<NaiveDate> = desired_dates.iter().copied().collect();
|
| 88 |
+
desired_days.sort();
|
| 89 |
+
|
| 90 |
+
Employee {
|
| 91 |
+
index,
|
| 92 |
+
name: self.name.clone(),
|
| 93 |
+
skills: self.skills.iter().cloned().collect(),
|
| 94 |
+
unavailable_dates,
|
| 95 |
+
undesired_dates,
|
| 96 |
+
desired_dates,
|
| 97 |
+
unavailable_days,
|
| 98 |
+
undesired_days,
|
| 99 |
+
desired_days,
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/// Shift DTO with embedded Employee object.
|
| 105 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 106 |
+
#[serde(rename_all = "camelCase")]
|
| 107 |
+
pub struct ShiftDto {
|
| 108 |
+
pub id: String,
|
| 109 |
+
pub start: NaiveDateTime,
|
| 110 |
+
pub end: NaiveDateTime,
|
| 111 |
+
pub location: String,
|
| 112 |
+
pub required_skill: String,
|
| 113 |
+
pub employee: Option<EmployeeDto>,
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/// Full schedule DTO for request/response.
|
| 117 |
+
#[derive(Debug, Serialize, Deserialize)]
|
| 118 |
+
#[serde(rename_all = "camelCase")]
|
| 119 |
+
pub struct ScheduleDto {
|
| 120 |
+
pub employees: Vec<EmployeeDto>,
|
| 121 |
+
pub shifts: Vec<ShiftDto>,
|
| 122 |
+
#[serde(default)]
|
| 123 |
+
pub score: Option<String>,
|
| 124 |
+
#[serde(default, skip_deserializing)]
|
| 125 |
+
pub solver_status: Option<String>,
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
impl ScheduleDto {
|
| 129 |
+
pub fn from_schedule(schedule: &EmployeeSchedule, solver_status: Option<String>) -> Self {
|
| 130 |
+
let employees: Vec<EmployeeDto> = schedule.employees.iter().map(EmployeeDto::from).collect();
|
| 131 |
+
|
| 132 |
+
let shifts: Vec<ShiftDto> = schedule
|
| 133 |
+
.shifts
|
| 134 |
+
.iter()
|
| 135 |
+
.map(|s| ShiftDto {
|
| 136 |
+
id: s.id.clone(),
|
| 137 |
+
start: s.start,
|
| 138 |
+
end: s.end,
|
| 139 |
+
location: s.location.clone(),
|
| 140 |
+
required_skill: s.required_skill.clone(),
|
| 141 |
+
employee: s.employee_idx
|
| 142 |
+
.and_then(|idx| schedule.employees.get(idx))
|
| 143 |
+
.map(EmployeeDto::from),
|
| 144 |
+
})
|
| 145 |
+
.collect();
|
| 146 |
+
|
| 147 |
+
Self {
|
| 148 |
+
employees,
|
| 149 |
+
shifts,
|
| 150 |
+
score: schedule.score.map(|s| format!("{}", s)),
|
| 151 |
+
solver_status,
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
pub fn to_domain(&self) -> EmployeeSchedule {
|
| 156 |
+
// Build employees with their indices set correctly
|
| 157 |
+
let employees: Vec<Employee> = self
|
| 158 |
+
.employees
|
| 159 |
+
.iter()
|
| 160 |
+
.enumerate()
|
| 161 |
+
.map(|(i, dto)| dto.to_employee(i))
|
| 162 |
+
.collect();
|
| 163 |
+
let name_to_idx: std::collections::HashMap<&str, usize> = employees
|
| 164 |
+
.iter()
|
| 165 |
+
.map(|e| (e.name.as_str(), e.index))
|
| 166 |
+
.collect();
|
| 167 |
+
|
| 168 |
+
let shifts: Vec<Shift> = self
|
| 169 |
+
.shifts
|
| 170 |
+
.iter()
|
| 171 |
+
.map(|s| Shift {
|
| 172 |
+
id: s.id.clone(),
|
| 173 |
+
start: s.start,
|
| 174 |
+
end: s.end,
|
| 175 |
+
location: s.location.clone(),
|
| 176 |
+
required_skill: s.required_skill.clone(),
|
| 177 |
+
employee_idx: s.employee.as_ref().and_then(|e| name_to_idx.get(e.name.as_str()).copied()),
|
| 178 |
+
})
|
| 179 |
+
.collect();
|
| 180 |
+
|
| 181 |
+
EmployeeSchedule::new(employees, shifts)
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// ============================================================================
|
| 186 |
+
// Router and Handlers
|
| 187 |
+
// ============================================================================
|
| 188 |
+
|
| 189 |
+
/// Creates the API router.
|
| 190 |
+
pub fn router(state: Arc<AppState>) -> Router {
|
| 191 |
+
Router::new()
|
| 192 |
+
// Health & Info
|
| 193 |
+
.route("/health", get(health))
|
| 194 |
+
.route("/info", get(info))
|
| 195 |
+
// Demo data
|
| 196 |
+
.route("/demo-data", get(list_demo_data))
|
| 197 |
+
.route("/demo-data/{id}", get(get_demo_data))
|
| 198 |
+
// Schedules
|
| 199 |
+
.route("/schedules", post(create_schedule))
|
| 200 |
+
.route("/schedules", get(list_schedules))
|
| 201 |
+
.route("/schedules/analyze", put(analyze_schedule))
|
| 202 |
+
.route("/schedules/{id}", get(get_schedule))
|
| 203 |
+
.route("/schedules/{id}/status", get(get_schedule_status))
|
| 204 |
+
.route("/schedules/{id}", delete(stop_solving))
|
| 205 |
+
.with_state(state)
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// ============================================================================
|
| 209 |
+
// Health & Info
|
| 210 |
+
// ============================================================================
|
| 211 |
+
|
| 212 |
+
#[derive(Debug, Serialize)]
|
| 213 |
+
pub struct HealthResponse {
|
| 214 |
+
pub status: &'static str,
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
/// GET /health - Health check endpoint.
|
| 218 |
+
async fn health() -> Json<HealthResponse> {
|
| 219 |
+
Json(HealthResponse { status: "UP" })
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
#[derive(Debug, Serialize)]
|
| 223 |
+
#[serde(rename_all = "camelCase")]
|
| 224 |
+
pub struct InfoResponse {
|
| 225 |
+
pub name: &'static str,
|
| 226 |
+
pub version: &'static str,
|
| 227 |
+
pub solver_engine: &'static str,
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/// GET /info - Application info endpoint.
|
| 231 |
+
async fn info() -> Json<InfoResponse> {
|
| 232 |
+
Json(InfoResponse {
|
| 233 |
+
name: "Employee Scheduling",
|
| 234 |
+
version: env!("CARGO_PKG_VERSION"),
|
| 235 |
+
solver_engine: "SolverForge-RS",
|
| 236 |
+
})
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/// GET /demo-data - List available demo data sets.
|
| 240 |
+
async fn list_demo_data() -> Json<Vec<&'static str>> {
|
| 241 |
+
Json(demo_data::list_demo_data())
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/// GET /demo-data/{id} - Get a specific demo data set.
|
| 245 |
+
async fn get_demo_data(Path(id): Path<String>) -> Result<Json<ScheduleDto>, StatusCode> {
|
| 246 |
+
match id.parse::<DemoData>() {
|
| 247 |
+
Ok(demo) => {
|
| 248 |
+
let schedule = demo_data::generate(demo);
|
| 249 |
+
Ok(Json(ScheduleDto::from_schedule(&schedule, None)))
|
| 250 |
+
}
|
| 251 |
+
Err(_) => Err(StatusCode::NOT_FOUND),
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
/// POST /schedules - Create and start solving a schedule.
|
| 256 |
+
/// Returns the job ID as plain text.
|
| 257 |
+
async fn create_schedule(
|
| 258 |
+
State(state): State<Arc<AppState>>,
|
| 259 |
+
Json(dto): Json<ScheduleDto>,
|
| 260 |
+
) -> String {
|
| 261 |
+
let id = Uuid::new_v4().to_string();
|
| 262 |
+
let schedule = dto.to_domain();
|
| 263 |
+
|
| 264 |
+
// Store initial state
|
| 265 |
+
{
|
| 266 |
+
let mut jobs = state.jobs.write();
|
| 267 |
+
jobs.insert(id.clone(), SolveJob {
|
| 268 |
+
solution: schedule.clone(),
|
| 269 |
+
solver_status: "SOLVING".to_string(),
|
| 270 |
+
});
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
// Start solving in background via library API
|
| 274 |
+
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
| 275 |
+
let job_id = id.clone();
|
| 276 |
+
let state_clone = state.clone();
|
| 277 |
+
|
| 278 |
+
tokio::spawn(async move {
|
| 279 |
+
while let Some((solution, _score)) = rx.recv().await {
|
| 280 |
+
let mut jobs = state_clone.jobs.write();
|
| 281 |
+
if let Some(job) = jobs.get_mut(&job_id) {
|
| 282 |
+
job.solution = solution;
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
// Channel closed - solver finished
|
| 286 |
+
let mut jobs = state_clone.jobs.write();
|
| 287 |
+
if let Some(job) = jobs.get_mut(&job_id) {
|
| 288 |
+
job.solver_status = "NOT_SOLVING".to_string();
|
| 289 |
+
}
|
| 290 |
+
});
|
| 291 |
+
|
| 292 |
+
// Solvable trait auto-implemented by #[planning_solution] macro
|
| 293 |
+
use solverforge::Solvable;
|
| 294 |
+
rayon::spawn(move || {
|
| 295 |
+
schedule.solve(None, tx);
|
| 296 |
+
});
|
| 297 |
+
|
| 298 |
+
id
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
/// GET /schedules - List all schedule IDs.
|
| 302 |
+
async fn list_schedules(State(state): State<Arc<AppState>>) -> Json<Vec<String>> {
|
| 303 |
+
Json(state.jobs.read().keys().cloned().collect())
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/// GET /schedules/{id} - Get a schedule's current state.
|
| 307 |
+
async fn get_schedule(
|
| 308 |
+
State(state): State<Arc<AppState>>,
|
| 309 |
+
Path(id): Path<String>,
|
| 310 |
+
) -> Result<Json<ScheduleDto>, StatusCode> {
|
| 311 |
+
match state.jobs.read().get(&id) {
|
| 312 |
+
Some(job) => {
|
| 313 |
+
Ok(Json(ScheduleDto::from_schedule(&job.solution, Some(job.solver_status.clone()))))
|
| 314 |
+
}
|
| 315 |
+
None => Err(StatusCode::NOT_FOUND),
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
/// Response for schedule status only.
|
| 320 |
+
#[derive(Debug, Serialize)]
|
| 321 |
+
#[serde(rename_all = "camelCase")]
|
| 322 |
+
pub struct StatusResponse {
|
| 323 |
+
pub score: Option<String>,
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
/// GET /schedules/{id}/status - Get a schedule's status.
|
| 327 |
+
async fn get_schedule_status(
|
| 328 |
+
State(state): State<Arc<AppState>>,
|
| 329 |
+
Path(id): Path<String>,
|
| 330 |
+
) -> Result<Json<StatusResponse>, StatusCode> {
|
| 331 |
+
match state.jobs.read().get(&id) {
|
| 332 |
+
Some(job) => {
|
| 333 |
+
Ok(Json(StatusResponse {
|
| 334 |
+
score: job.solution.score.map(|s| format!("{}", s)),
|
| 335 |
+
}))
|
| 336 |
+
}
|
| 337 |
+
None => Err(StatusCode::NOT_FOUND),
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
/// DELETE /schedules/{id} - Stop solving and remove a schedule.
|
| 342 |
+
async fn stop_solving(
|
| 343 |
+
State(state): State<Arc<AppState>>,
|
| 344 |
+
Path(id): Path<String>,
|
| 345 |
+
) -> StatusCode {
|
| 346 |
+
if state.jobs.write().remove(&id).is_some() {
|
| 347 |
+
StatusCode::NO_CONTENT
|
| 348 |
+
} else {
|
| 349 |
+
StatusCode::NOT_FOUND
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
/// Constraint analysis result.
|
| 354 |
+
#[derive(Debug, Serialize)]
|
| 355 |
+
#[serde(rename_all = "camelCase")]
|
| 356 |
+
pub struct ConstraintAnalysisDto {
|
| 357 |
+
pub name: String,
|
| 358 |
+
#[serde(rename = "type")]
|
| 359 |
+
pub constraint_type: String,
|
| 360 |
+
pub weight: String,
|
| 361 |
+
pub score: String,
|
| 362 |
+
pub matches: Vec<ConstraintMatchDto>,
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
/// A single constraint match.
|
| 366 |
+
#[derive(Debug, Serialize)]
|
| 367 |
+
#[serde(rename_all = "camelCase")]
|
| 368 |
+
pub struct ConstraintMatchDto {
|
| 369 |
+
pub score: String,
|
| 370 |
+
pub justification: String,
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
/// Response for constraint analysis.
|
| 374 |
+
#[derive(Debug, Serialize)]
|
| 375 |
+
#[serde(rename_all = "camelCase")]
|
| 376 |
+
pub struct AnalyzeResponse {
|
| 377 |
+
pub score: String,
|
| 378 |
+
pub constraints: Vec<ConstraintAnalysisDto>,
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/// PUT /schedules/analyze - Analyze constraints for a schedule.
|
| 382 |
+
///
|
| 383 |
+
/// Uses TypedScoreDirector for incremental scoring.
|
| 384 |
+
async fn analyze_schedule(
|
| 385 |
+
Json(dto): Json<ScheduleDto>,
|
| 386 |
+
) -> Json<AnalyzeResponse> {
|
| 387 |
+
use crate::constraints::create_fluent_constraints;
|
| 388 |
+
use solverforge::{ConstraintSet, TypedScoreDirector};
|
| 389 |
+
|
| 390 |
+
let schedule = dto.to_domain();
|
| 391 |
+
|
| 392 |
+
// Use fluent API constraints for zero-erasure scoring
|
| 393 |
+
let constraints = create_fluent_constraints();
|
| 394 |
+
let mut director = TypedScoreDirector::new(schedule, constraints);
|
| 395 |
+
|
| 396 |
+
let score = director.calculate_score();
|
| 397 |
+
|
| 398 |
+
// Get per-constraint breakdown with detailed matches
|
| 399 |
+
let analyses = director.constraints().evaluate_detailed(director.working_solution());
|
| 400 |
+
|
| 401 |
+
let constraints_dto: Vec<ConstraintAnalysisDto> = analyses
|
| 402 |
+
.into_iter()
|
| 403 |
+
.map(|analysis| {
|
| 404 |
+
ConstraintAnalysisDto {
|
| 405 |
+
name: analysis.constraint_ref.name.clone(),
|
| 406 |
+
constraint_type: if analysis.is_hard { "hard" } else { "soft" }.to_string(),
|
| 407 |
+
weight: format!("{}", analysis.weight),
|
| 408 |
+
score: format!("{}", analysis.score),
|
| 409 |
+
matches: analysis
|
| 410 |
+
.matches
|
| 411 |
+
.iter()
|
| 412 |
+
.map(|m| ConstraintMatchDto {
|
| 413 |
+
score: format!("{}", m.score),
|
| 414 |
+
justification: m.justification.description.clone(),
|
| 415 |
+
})
|
| 416 |
+
.collect(),
|
| 417 |
+
}
|
| 418 |
+
})
|
| 419 |
+
.collect();
|
| 420 |
+
|
| 421 |
+
Json(AnalyzeResponse {
|
| 422 |
+
score: format!("{}", score),
|
| 423 |
+
constraints: constraints_dto,
|
| 424 |
+
})
|
| 425 |
+
}
|
src/bin/bench.rs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Benchmark for incremental scoring performance.
|
| 2 |
+
//!
|
| 3 |
+
//! Run with: cargo run --release -p employee-scheduling --bin bench
|
| 4 |
+
|
| 5 |
+
use employee_scheduling::{constraints, demo_data};
|
| 6 |
+
use solverforge::TypedScoreDirector;
|
| 7 |
+
use std::time::Instant;
|
| 8 |
+
|
| 9 |
+
fn main() {
|
| 10 |
+
let schedule = demo_data::generate(demo_data::DemoData::Large);
|
| 11 |
+
let n_shifts = schedule.shifts.len();
|
| 12 |
+
let n_employees = schedule.employees.len();
|
| 13 |
+
|
| 14 |
+
println!("Benchmark: Incremental Scoring (Fluent API)");
|
| 15 |
+
println!(" Shifts: {}", n_shifts);
|
| 16 |
+
println!(" Employees: {}", n_employees);
|
| 17 |
+
println!();
|
| 18 |
+
|
| 19 |
+
let constraint_set = constraints::create_fluent_constraints();
|
| 20 |
+
let mut director = TypedScoreDirector::new(schedule, constraint_set);
|
| 21 |
+
|
| 22 |
+
// Initialize
|
| 23 |
+
let init_start = Instant::now();
|
| 24 |
+
let initial_score = director.calculate_score();
|
| 25 |
+
println!("Initial score: {} ({:?})", initial_score, init_start.elapsed());
|
| 26 |
+
println!();
|
| 27 |
+
|
| 28 |
+
// Benchmark: deterministic do/undo cycle for each shift×employee combination
|
| 29 |
+
// This measures pure incremental scoring throughput
|
| 30 |
+
let bench_start = Instant::now();
|
| 31 |
+
let mut moves: u64 = 0;
|
| 32 |
+
|
| 33 |
+
for shift_idx in 0..n_shifts {
|
| 34 |
+
let old_idx = director.working_solution().shifts[shift_idx].employee_idx;
|
| 35 |
+
|
| 36 |
+
for emp_idx in 0..n_employees {
|
| 37 |
+
// Do move
|
| 38 |
+
director.before_variable_changed(shift_idx);
|
| 39 |
+
director.working_solution_mut().shifts[shift_idx].employee_idx = Some(emp_idx);
|
| 40 |
+
director.after_variable_changed(shift_idx);
|
| 41 |
+
let _ = director.get_score();
|
| 42 |
+
moves += 1;
|
| 43 |
+
|
| 44 |
+
// Undo move
|
| 45 |
+
director.before_variable_changed(shift_idx);
|
| 46 |
+
director.working_solution_mut().shifts[shift_idx].employee_idx = old_idx;
|
| 47 |
+
director.after_variable_changed(shift_idx);
|
| 48 |
+
let _ = director.get_score();
|
| 49 |
+
moves += 1;
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
let elapsed = bench_start.elapsed();
|
| 54 |
+
let moves_per_sec = moves as f64 / elapsed.as_secs_f64();
|
| 55 |
+
|
| 56 |
+
println!("Results:");
|
| 57 |
+
println!(" Moves: {}", moves);
|
| 58 |
+
println!(" Time: {:.2?}", elapsed);
|
| 59 |
+
println!(" Moves/sec: {:.0}", moves_per_sec);
|
| 60 |
+
|
| 61 |
+
// Verify score unchanged after all do/undo cycles
|
| 62 |
+
let final_score = director.get_score();
|
| 63 |
+
assert_eq!(initial_score, final_score, "Score corrupted!");
|
| 64 |
+
println!(" Final score: {} (verified)", final_score);
|
| 65 |
+
}
|
src/console.rs
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Colorful console output for solver metrics.
|
| 2 |
+
|
| 3 |
+
use num_format::{Locale, ToFormattedString};
|
| 4 |
+
use owo_colors::OwoColorize;
|
| 5 |
+
use std::time::{Duration, Instant};
|
| 6 |
+
|
| 7 |
+
/// ASCII art banner for solver startup.
|
| 8 |
+
pub fn print_banner() {
|
| 9 |
+
let banner = r#"
|
| 10 |
+
____ _ _____
|
| 11 |
+
/ ___| ___ | |_ _____ _ __| ___|__ _ __ __ _ ___
|
| 12 |
+
\___ \ / _ \| \ \ / / _ \ '__| |_ / _ \| '__/ _` |/ _ \
|
| 13 |
+
___) | (_) | |\ V / __/ | | _| (_) | | | (_| | __/
|
| 14 |
+
|____/ \___/|_| \_/ \___|_| |_| \___/|_| \__, |\___|
|
| 15 |
+
|___/
|
| 16 |
+
"#;
|
| 17 |
+
println!("{}", banner.cyan().bold());
|
| 18 |
+
println!(
|
| 19 |
+
" {} {}\n",
|
| 20 |
+
format!("v{}", env!("CARGO_PKG_VERSION")).bright_black(),
|
| 21 |
+
"Employee Scheduling".bright_cyan()
|
| 22 |
+
);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/// Prints "Solving started" message.
|
| 26 |
+
pub fn print_solving_started(
|
| 27 |
+
time_spent_ms: u64,
|
| 28 |
+
best_score: &str,
|
| 29 |
+
entity_count: usize,
|
| 30 |
+
variable_count: usize,
|
| 31 |
+
value_count: usize,
|
| 32 |
+
) {
|
| 33 |
+
println!(
|
| 34 |
+
"{} {} {} time spent ({}), best score ({}), random ({})",
|
| 35 |
+
timestamp().bright_black(),
|
| 36 |
+
"INFO".bright_green(),
|
| 37 |
+
"[Solver]".bright_cyan(),
|
| 38 |
+
format!("{}ms", time_spent_ms).yellow(),
|
| 39 |
+
format_score(best_score),
|
| 40 |
+
"StdRng".white()
|
| 41 |
+
);
|
| 42 |
+
|
| 43 |
+
// Problem scale
|
| 44 |
+
let scale = calculate_problem_scale(entity_count, value_count);
|
| 45 |
+
println!(
|
| 46 |
+
"{} {} {} entity count ({}), variable count ({}), value count ({}), problem scale ({})",
|
| 47 |
+
timestamp().bright_black(),
|
| 48 |
+
"INFO".bright_green(),
|
| 49 |
+
"[Solver]".bright_cyan(),
|
| 50 |
+
entity_count.to_formatted_string(&Locale::en).bright_yellow(),
|
| 51 |
+
variable_count.to_formatted_string(&Locale::en).bright_yellow(),
|
| 52 |
+
value_count.to_formatted_string(&Locale::en).bright_yellow(),
|
| 53 |
+
scale.bright_magenta()
|
| 54 |
+
);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/// Prints a phase start message.
|
| 58 |
+
pub fn print_phase_start(phase_name: &str, phase_index: usize) {
|
| 59 |
+
println!(
|
| 60 |
+
"{} {} {} {} phase ({}) started",
|
| 61 |
+
timestamp().bright_black(),
|
| 62 |
+
"INFO".bright_green(),
|
| 63 |
+
format!("[{}]", phase_name).bright_cyan(),
|
| 64 |
+
phase_name.white().bold(),
|
| 65 |
+
phase_index.to_string().yellow()
|
| 66 |
+
);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/// Prints a phase end message with metrics.
|
| 70 |
+
pub fn print_phase_end(
|
| 71 |
+
phase_name: &str,
|
| 72 |
+
phase_index: usize,
|
| 73 |
+
duration: Duration,
|
| 74 |
+
steps_accepted: u64,
|
| 75 |
+
moves_evaluated: u64,
|
| 76 |
+
best_score: &str,
|
| 77 |
+
) {
|
| 78 |
+
let moves_per_sec = if duration.as_secs_f64() > 0.0 {
|
| 79 |
+
(moves_evaluated as f64 / duration.as_secs_f64()) as u64
|
| 80 |
+
} else {
|
| 81 |
+
0
|
| 82 |
+
};
|
| 83 |
+
let acceptance_rate = if moves_evaluated > 0 {
|
| 84 |
+
(steps_accepted as f64 / moves_evaluated as f64) * 100.0
|
| 85 |
+
} else {
|
| 86 |
+
0.0
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
println!(
|
| 90 |
+
"{} {} {} {} phase ({}) ended: time spent ({}), best score ({}), move evaluation speed ({}/sec), step total ({}, {:.1}% accepted)",
|
| 91 |
+
timestamp().bright_black(),
|
| 92 |
+
"INFO".bright_green(),
|
| 93 |
+
format!("[{}]", phase_name).bright_cyan(),
|
| 94 |
+
phase_name.white().bold(),
|
| 95 |
+
phase_index.to_string().yellow(),
|
| 96 |
+
format_duration(duration).yellow(),
|
| 97 |
+
format_score(best_score),
|
| 98 |
+
moves_per_sec.to_formatted_string(&Locale::en).bright_magenta().bold(),
|
| 99 |
+
steps_accepted.to_formatted_string(&Locale::en).white(),
|
| 100 |
+
acceptance_rate
|
| 101 |
+
);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/// Prints a step progress update with moves/sec prominently displayed.
|
| 105 |
+
pub fn print_step_progress(
|
| 106 |
+
step: u64,
|
| 107 |
+
elapsed: Duration,
|
| 108 |
+
moves_evaluated: u64,
|
| 109 |
+
score: &str,
|
| 110 |
+
) {
|
| 111 |
+
let moves_per_sec = if elapsed.as_secs_f64() > 0.0 {
|
| 112 |
+
(moves_evaluated as f64 / elapsed.as_secs_f64()) as u64
|
| 113 |
+
} else {
|
| 114 |
+
0
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
println!(
|
| 118 |
+
" {} Step {:>7} │ {} │ {}/sec │ {}",
|
| 119 |
+
"→".bright_blue(),
|
| 120 |
+
step.to_formatted_string(&Locale::en).white(),
|
| 121 |
+
format!("{:>6}", format_duration(elapsed)).bright_black(),
|
| 122 |
+
format!("{:>8}", moves_per_sec.to_formatted_string(&Locale::en)).bright_magenta().bold(),
|
| 123 |
+
format_score(score)
|
| 124 |
+
);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/// Prints solver completion summary.
|
| 128 |
+
pub fn print_solving_ended(
|
| 129 |
+
total_duration: Duration,
|
| 130 |
+
total_moves: u64,
|
| 131 |
+
phase_count: usize,
|
| 132 |
+
final_score: &str,
|
| 133 |
+
is_feasible: bool,
|
| 134 |
+
) {
|
| 135 |
+
let moves_per_sec = if total_duration.as_secs_f64() > 0.0 {
|
| 136 |
+
(total_moves as f64 / total_duration.as_secs_f64()) as u64
|
| 137 |
+
} else {
|
| 138 |
+
0
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
println!(
|
| 142 |
+
"{} {} {} Solving ended: time spent ({}), best score ({}), move evaluation speed ({}/sec), phase total ({})",
|
| 143 |
+
timestamp().bright_black(),
|
| 144 |
+
"INFO".bright_green(),
|
| 145 |
+
"[Solver]".bright_cyan(),
|
| 146 |
+
format_duration(total_duration).yellow(),
|
| 147 |
+
format_score(final_score),
|
| 148 |
+
moves_per_sec.to_formatted_string(&Locale::en).bright_magenta().bold(),
|
| 149 |
+
phase_count.to_string().white()
|
| 150 |
+
);
|
| 151 |
+
|
| 152 |
+
// Pretty summary box (60 chars wide, 56 char content area)
|
| 153 |
+
println!();
|
| 154 |
+
println!("{}", "╔════════════════════���═════════════════════════════════════╗".bright_cyan());
|
| 155 |
+
|
| 156 |
+
let status_text = if is_feasible {
|
| 157 |
+
"✓ FEASIBLE SOLUTION FOUND"
|
| 158 |
+
} else {
|
| 159 |
+
"✗ INFEASIBLE (hard constraints violated)"
|
| 160 |
+
};
|
| 161 |
+
let status_colored = if is_feasible {
|
| 162 |
+
status_text.bright_green().bold().to_string()
|
| 163 |
+
} else {
|
| 164 |
+
status_text.bright_red().bold().to_string()
|
| 165 |
+
};
|
| 166 |
+
let status_padding = 56 - status_text.chars().count();
|
| 167 |
+
let left_pad = status_padding / 2;
|
| 168 |
+
let right_pad = status_padding - left_pad;
|
| 169 |
+
println!(
|
| 170 |
+
"{}{}{}{}{}",
|
| 171 |
+
"║".bright_cyan(),
|
| 172 |
+
" ".repeat(left_pad),
|
| 173 |
+
status_colored,
|
| 174 |
+
" ".repeat(right_pad),
|
| 175 |
+
"║".bright_cyan()
|
| 176 |
+
);
|
| 177 |
+
|
| 178 |
+
println!("{}", "╠══════════════════════════════════════════════════════════╣".bright_cyan());
|
| 179 |
+
|
| 180 |
+
let score_str = final_score;
|
| 181 |
+
println!(
|
| 182 |
+
"{} {:<18}{:>36} {}",
|
| 183 |
+
"║".bright_cyan(),
|
| 184 |
+
"Final Score:",
|
| 185 |
+
score_str,
|
| 186 |
+
"║".bright_cyan()
|
| 187 |
+
);
|
| 188 |
+
|
| 189 |
+
let time_str = format!("{:.2}s", total_duration.as_secs_f64());
|
| 190 |
+
println!(
|
| 191 |
+
"{} {:<18}{:>36} {}",
|
| 192 |
+
"║".bright_cyan(),
|
| 193 |
+
"Solving Time:",
|
| 194 |
+
time_str,
|
| 195 |
+
"║".bright_cyan()
|
| 196 |
+
);
|
| 197 |
+
|
| 198 |
+
let speed_str = format!("{}/sec", moves_per_sec.to_formatted_string(&Locale::en));
|
| 199 |
+
println!(
|
| 200 |
+
"{} {:<18}{:>36} {}",
|
| 201 |
+
"║".bright_cyan(),
|
| 202 |
+
"Move Speed:",
|
| 203 |
+
speed_str,
|
| 204 |
+
"║".bright_cyan()
|
| 205 |
+
);
|
| 206 |
+
|
| 207 |
+
println!("{}", "╚══════════════════════════════════════════════════════════╝".bright_cyan());
|
| 208 |
+
println!();
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/// Prints constraint analysis results.
|
| 212 |
+
pub fn print_constraint_analysis(constraints: &[(String, String, usize)]) {
|
| 213 |
+
println!(
|
| 214 |
+
"{} {} {} Constraint Analysis:",
|
| 215 |
+
timestamp().bright_black(),
|
| 216 |
+
"INFO".bright_green(),
|
| 217 |
+
"[Solver]".bright_cyan()
|
| 218 |
+
);
|
| 219 |
+
|
| 220 |
+
for (name, score, match_count) in constraints {
|
| 221 |
+
let status = if score.starts_with("0") || score == "0hard/0soft" {
|
| 222 |
+
"✓".bright_green().to_string()
|
| 223 |
+
} else if score.contains("hard") && !score.starts_with("0hard") {
|
| 224 |
+
"✗".bright_red().to_string()
|
| 225 |
+
} else {
|
| 226 |
+
"○".yellow().to_string()
|
| 227 |
+
};
|
| 228 |
+
|
| 229 |
+
println!(
|
| 230 |
+
" {} {:<40} {:>15} ({} matches)",
|
| 231 |
+
status,
|
| 232 |
+
name.white(),
|
| 233 |
+
format_score(score),
|
| 234 |
+
match_count.to_formatted_string(&Locale::en).bright_black()
|
| 235 |
+
);
|
| 236 |
+
}
|
| 237 |
+
println!();
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
/// Prints the solver configuration.
|
| 241 |
+
pub fn print_config(shifts: usize, employees: usize) {
|
| 242 |
+
println!(
|
| 243 |
+
"{} {} {} Solver configuration: shifts ({}), employees ({})",
|
| 244 |
+
timestamp().bright_black(),
|
| 245 |
+
"INFO".bright_green(),
|
| 246 |
+
"[Solver]".bright_cyan(),
|
| 247 |
+
shifts.to_formatted_string(&Locale::en).bright_yellow(),
|
| 248 |
+
employees.to_formatted_string(&Locale::en).bright_yellow()
|
| 249 |
+
);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/// Formats a duration nicely.
|
| 253 |
+
fn format_duration(d: Duration) -> String {
|
| 254 |
+
let total_ms = d.as_millis();
|
| 255 |
+
if total_ms < 1000 {
|
| 256 |
+
format!("{}ms", total_ms)
|
| 257 |
+
} else if total_ms < 60_000 {
|
| 258 |
+
format!("{:.2}s", d.as_secs_f64())
|
| 259 |
+
} else {
|
| 260 |
+
let mins = total_ms / 60_000;
|
| 261 |
+
let secs = (total_ms % 60_000) / 1000;
|
| 262 |
+
format!("{}m {}s", mins, secs)
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
/// Formats a score with colors based on feasibility.
|
| 267 |
+
fn format_score(score: &str) -> String {
|
| 268 |
+
// Parse HardSoftScore format like "-2hard/5soft" or "0hard/10soft"
|
| 269 |
+
if score.contains("hard") {
|
| 270 |
+
let parts: Vec<&str> = score.split('/').collect();
|
| 271 |
+
if parts.len() == 2 {
|
| 272 |
+
let hard = parts[0].trim_end_matches("hard");
|
| 273 |
+
let soft = parts[1].trim_end_matches("soft");
|
| 274 |
+
|
| 275 |
+
let hard_num: f64 = hard.parse().unwrap_or(0.0);
|
| 276 |
+
let soft_num: f64 = soft.parse().unwrap_or(0.0);
|
| 277 |
+
|
| 278 |
+
let hard_str = if hard_num < 0.0 {
|
| 279 |
+
format!("{}hard", hard).bright_red().to_string()
|
| 280 |
+
} else {
|
| 281 |
+
format!("{}hard", hard).bright_green().to_string()
|
| 282 |
+
};
|
| 283 |
+
|
| 284 |
+
let soft_str = if soft_num < 0.0 {
|
| 285 |
+
format!("{}soft", soft).yellow().to_string()
|
| 286 |
+
} else if soft_num > 0.0 {
|
| 287 |
+
format!("{}soft", soft).bright_green().to_string()
|
| 288 |
+
} else {
|
| 289 |
+
format!("{}soft", soft).white().to_string()
|
| 290 |
+
};
|
| 291 |
+
|
| 292 |
+
return format!("{}/{}", hard_str, soft_str);
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
// Simple score
|
| 297 |
+
if let Ok(n) = score.parse::<i32>() {
|
| 298 |
+
if n < 0 {
|
| 299 |
+
return score.bright_red().to_string();
|
| 300 |
+
} else if n > 0 {
|
| 301 |
+
return score.bright_green().to_string();
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
score.white().to_string()
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/// Returns a timestamp string.
|
| 309 |
+
fn timestamp() -> String {
|
| 310 |
+
std::time::SystemTime::now()
|
| 311 |
+
.duration_since(std::time::UNIX_EPOCH)
|
| 312 |
+
.map(|d| {
|
| 313 |
+
let secs = d.as_secs();
|
| 314 |
+
let millis = d.subsec_millis();
|
| 315 |
+
format!("{}.{:03}", secs, millis)
|
| 316 |
+
})
|
| 317 |
+
.unwrap_or_else(|_| "0.000".to_string())
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/// Calculates an approximate problem scale.
|
| 321 |
+
fn calculate_problem_scale(entity_count: usize, value_count: usize) -> String {
|
| 322 |
+
if entity_count == 0 || value_count == 0 {
|
| 323 |
+
return "0".to_string();
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// value_count ^ entity_count
|
| 327 |
+
let log_scale = (entity_count as f64) * (value_count as f64).log10();
|
| 328 |
+
let exponent = log_scale.floor() as i32;
|
| 329 |
+
let mantissa = 10f64.powf(log_scale - exponent as f64);
|
| 330 |
+
|
| 331 |
+
format!("{:.3} × 10^{}", mantissa, exponent)
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
/// A timer for tracking phase/step durations.
|
| 335 |
+
pub struct PhaseTimer {
|
| 336 |
+
start: Instant,
|
| 337 |
+
phase_name: String,
|
| 338 |
+
phase_index: usize,
|
| 339 |
+
steps_accepted: u64,
|
| 340 |
+
moves_evaluated: u64,
|
| 341 |
+
last_score: String,
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
impl PhaseTimer {
|
| 345 |
+
pub fn start(phase_name: impl Into<String>, phase_index: usize) -> Self {
|
| 346 |
+
let name = phase_name.into();
|
| 347 |
+
print_phase_start(&name, phase_index);
|
| 348 |
+
Self {
|
| 349 |
+
start: Instant::now(),
|
| 350 |
+
phase_name: name,
|
| 351 |
+
phase_index,
|
| 352 |
+
steps_accepted: 0,
|
| 353 |
+
moves_evaluated: 0,
|
| 354 |
+
last_score: String::new(),
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
pub fn record_accepted(&mut self, score: &str) {
|
| 359 |
+
self.steps_accepted += 1;
|
| 360 |
+
self.last_score = score.to_string();
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
pub fn record_move(&mut self) {
|
| 364 |
+
self.moves_evaluated += 1;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
pub fn elapsed(&self) -> Duration {
|
| 368 |
+
self.start.elapsed()
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
pub fn moves_evaluated(&self) -> u64 {
|
| 372 |
+
self.moves_evaluated
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
pub fn finish(self) {
|
| 376 |
+
print_phase_end(
|
| 377 |
+
&self.phase_name,
|
| 378 |
+
self.phase_index,
|
| 379 |
+
self.start.elapsed(),
|
| 380 |
+
self.steps_accepted,
|
| 381 |
+
self.moves_evaluated,
|
| 382 |
+
&self.last_score,
|
| 383 |
+
);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
pub fn steps_accepted(&self) -> u64 {
|
| 387 |
+
self.steps_accepted
|
| 388 |
+
}
|
| 389 |
+
}
|
src/constraints.rs
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Zero-erasure constraints for Employee Scheduling using fluent API.
|
| 2 |
+
//!
|
| 3 |
+
//! All constraints use the fluent constraint stream API with concrete generic
|
| 4 |
+
//! types - no Arc, no dyn, fully monomorphized.
|
| 5 |
+
|
| 6 |
+
use chrono::NaiveDate;
|
| 7 |
+
use solverforge::prelude::*;
|
| 8 |
+
use solverforge::stream::joiner::equal_bi;
|
| 9 |
+
|
| 10 |
+
use crate::domain::{Employee, EmployeeSchedule, Shift};
|
| 11 |
+
|
| 12 |
+
/// Creates all constraints using the fluent API (fully monomorphized).
|
| 13 |
+
pub fn create_fluent_constraints() -> impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore> {
|
| 14 |
+
let factory = ConstraintFactory::<EmployeeSchedule, HardSoftDecimalScore>::new();
|
| 15 |
+
|
| 16 |
+
// =========================================================================
|
| 17 |
+
// HARD: Required Skill
|
| 18 |
+
// =========================================================================
|
| 19 |
+
let required_skill = factory
|
| 20 |
+
.clone()
|
| 21 |
+
.for_each(|s: &EmployeeSchedule| s.shifts.as_slice())
|
| 22 |
+
.join(
|
| 23 |
+
|s: &EmployeeSchedule| s.employees.as_slice(),
|
| 24 |
+
equal_bi(
|
| 25 |
+
|shift: &Shift| shift.employee_idx,
|
| 26 |
+
|emp: &Employee| Some(emp.index),
|
| 27 |
+
),
|
| 28 |
+
)
|
| 29 |
+
.filter(|shift: &Shift, emp: &Employee| {
|
| 30 |
+
shift.employee_idx.is_some() && !emp.skills.contains(&shift.required_skill)
|
| 31 |
+
})
|
| 32 |
+
.penalize(HardSoftDecimalScore::ONE_HARD)
|
| 33 |
+
.as_constraint("Required skill");
|
| 34 |
+
|
| 35 |
+
// =========================================================================
|
| 36 |
+
// HARD: No Overlapping Shifts
|
| 37 |
+
// =========================================================================
|
| 38 |
+
// Note: overlapping joiner can't be composed with equality joiner for self-joins
|
| 39 |
+
// because for_each_unique_pair requires EqualJoiner for hash indexing.
|
| 40 |
+
// The filter approach is correct for self-join overlap detection.
|
| 41 |
+
let no_overlap = factory
|
| 42 |
+
.clone()
|
| 43 |
+
.for_each_unique_pair(
|
| 44 |
+
|s: &EmployeeSchedule| s.shifts.as_slice(),
|
| 45 |
+
joiner::equal(|shift: &Shift| shift.employee_idx),
|
| 46 |
+
)
|
| 47 |
+
.filter(|a: &Shift, b: &Shift| {
|
| 48 |
+
a.employee_idx.is_some() && a.start < b.end && b.start < a.end
|
| 49 |
+
})
|
| 50 |
+
.penalize_hard_with(|a: &Shift, b: &Shift| {
|
| 51 |
+
HardSoftDecimalScore::of_hard_scaled(overlap_minutes(a, b) * 100000)
|
| 52 |
+
})
|
| 53 |
+
.as_constraint("Overlapping shift");
|
| 54 |
+
|
| 55 |
+
// =========================================================================
|
| 56 |
+
// HARD: At Least 10 Hours Between Shifts
|
| 57 |
+
// =========================================================================
|
| 58 |
+
let at_least_10_hours = factory
|
| 59 |
+
.clone()
|
| 60 |
+
.for_each_unique_pair(
|
| 61 |
+
|s: &EmployeeSchedule| s.shifts.as_slice(),
|
| 62 |
+
joiner::equal(|shift: &Shift| shift.employee_idx),
|
| 63 |
+
)
|
| 64 |
+
.filter(|a: &Shift, b: &Shift| a.employee_idx.is_some() && gap_penalty_minutes(a, b) > 0)
|
| 65 |
+
.penalize_hard_with(|a: &Shift, b: &Shift| {
|
| 66 |
+
HardSoftDecimalScore::of_hard_scaled(gap_penalty_minutes(a, b) * 100000)
|
| 67 |
+
})
|
| 68 |
+
.as_constraint("At least 10 hours between 2 shifts");
|
| 69 |
+
|
| 70 |
+
// =========================================================================
|
| 71 |
+
// HARD: One Shift Per Day
|
| 72 |
+
// =========================================================================
|
| 73 |
+
let one_per_day = factory
|
| 74 |
+
.clone()
|
| 75 |
+
.for_each_unique_pair(
|
| 76 |
+
|s: &EmployeeSchedule| s.shifts.as_slice(),
|
| 77 |
+
joiner::equal(|shift: &Shift| (shift.employee_idx, shift.date())),
|
| 78 |
+
)
|
| 79 |
+
.filter(|a: &Shift, b: &Shift| a.employee_idx.is_some() && b.employee_idx.is_some())
|
| 80 |
+
.penalize(HardSoftDecimalScore::ONE_HARD)
|
| 81 |
+
.as_constraint("One shift per day");
|
| 82 |
+
|
| 83 |
+
// =========================================================================
|
| 84 |
+
// HARD: Unavailable Employee
|
| 85 |
+
// =========================================================================
|
| 86 |
+
// Uses flatten_last for O(1) lookup by date.
|
| 87 |
+
// Pre-indexes unavailable dates, looks up by shift.date() in O(1).
|
| 88 |
+
let unavailable = factory
|
| 89 |
+
.clone()
|
| 90 |
+
.for_each(|s: &EmployeeSchedule| s.shifts.as_slice())
|
| 91 |
+
.join(
|
| 92 |
+
|s: &EmployeeSchedule| s.employees.as_slice(),
|
| 93 |
+
equal_bi(
|
| 94 |
+
|shift: &Shift| shift.employee_idx,
|
| 95 |
+
|emp: &Employee| Some(emp.index),
|
| 96 |
+
),
|
| 97 |
+
)
|
| 98 |
+
.flatten_last(
|
| 99 |
+
|emp: &Employee| emp.unavailable_days.as_slice(),
|
| 100 |
+
|date: &NaiveDate| *date, // C → index key
|
| 101 |
+
|shift: &Shift| shift.date(), // A → lookup key
|
| 102 |
+
)
|
| 103 |
+
.filter(|shift: &Shift, date: &NaiveDate| {
|
| 104 |
+
shift.employee_idx.is_some() && shift_date_overlap_minutes(shift, *date) > 0
|
| 105 |
+
})
|
| 106 |
+
.penalize_hard_with(|shift: &Shift, date: &NaiveDate| {
|
| 107 |
+
HardSoftDecimalScore::of_hard_scaled(shift_date_overlap_minutes(shift, *date) * 100000)
|
| 108 |
+
})
|
| 109 |
+
.as_constraint("Unavailable employee");
|
| 110 |
+
|
| 111 |
+
// =========================================================================
|
| 112 |
+
// SOFT: Undesired Day
|
| 113 |
+
// =========================================================================
|
| 114 |
+
// Uses flatten_last for O(1) lookup. Penalizes 1 per match (Timefold pattern).
|
| 115 |
+
let undesired = factory
|
| 116 |
+
.clone()
|
| 117 |
+
.for_each(|s: &EmployeeSchedule| s.shifts.as_slice())
|
| 118 |
+
.join(
|
| 119 |
+
|s: &EmployeeSchedule| s.employees.as_slice(),
|
| 120 |
+
equal_bi(
|
| 121 |
+
|shift: &Shift| shift.employee_idx,
|
| 122 |
+
|emp: &Employee| Some(emp.index),
|
| 123 |
+
),
|
| 124 |
+
)
|
| 125 |
+
.flatten_last(
|
| 126 |
+
|emp: &Employee| emp.undesired_days.as_slice(),
|
| 127 |
+
|date: &NaiveDate| *date,
|
| 128 |
+
|shift: &Shift| shift.date(),
|
| 129 |
+
)
|
| 130 |
+
.filter(|shift: &Shift, _date: &NaiveDate| shift.employee_idx.is_some())
|
| 131 |
+
.penalize(HardSoftDecimalScore::ONE_SOFT)
|
| 132 |
+
.as_constraint("Undesired day for employee");
|
| 133 |
+
|
| 134 |
+
// =========================================================================
|
| 135 |
+
// SOFT: Desired Day
|
| 136 |
+
// =========================================================================
|
| 137 |
+
// Uses flatten_last for O(1) lookup. Rewards 1 per match (Timefold pattern).
|
| 138 |
+
let desired = factory
|
| 139 |
+
.clone()
|
| 140 |
+
.for_each(|s: &EmployeeSchedule| s.shifts.as_slice())
|
| 141 |
+
.join(
|
| 142 |
+
|s: &EmployeeSchedule| s.employees.as_slice(),
|
| 143 |
+
equal_bi(
|
| 144 |
+
|shift: &Shift| shift.employee_idx,
|
| 145 |
+
|emp: &Employee| Some(emp.index),
|
| 146 |
+
),
|
| 147 |
+
)
|
| 148 |
+
.flatten_last(
|
| 149 |
+
|emp: &Employee| emp.desired_days.as_slice(),
|
| 150 |
+
|date: &NaiveDate| *date,
|
| 151 |
+
|shift: &Shift| shift.date(),
|
| 152 |
+
)
|
| 153 |
+
.filter(|shift: &Shift, _date: &NaiveDate| shift.employee_idx.is_some())
|
| 154 |
+
.reward(HardSoftDecimalScore::ONE_SOFT)
|
| 155 |
+
.as_constraint("Desired day for employee");
|
| 156 |
+
|
| 157 |
+
// =========================================================================
|
| 158 |
+
// SOFT: Balance Assignments
|
| 159 |
+
// =========================================================================
|
| 160 |
+
// Uses simple balance() for O(1) incremental std-dev calculation.
|
| 161 |
+
let balanced = factory
|
| 162 |
+
.for_each(|s: &EmployeeSchedule| s.shifts.as_slice())
|
| 163 |
+
.balance(|shift: &Shift| shift.employee_idx)
|
| 164 |
+
.penalize(HardSoftDecimalScore::of_soft(1))
|
| 165 |
+
.as_constraint("Balance employee assignments");
|
| 166 |
+
|
| 167 |
+
(
|
| 168 |
+
required_skill,
|
| 169 |
+
no_overlap,
|
| 170 |
+
at_least_10_hours,
|
| 171 |
+
one_per_day,
|
| 172 |
+
unavailable,
|
| 173 |
+
undesired,
|
| 174 |
+
desired,
|
| 175 |
+
balanced,
|
| 176 |
+
)
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// ============================================================================
|
| 180 |
+
// Helper functions
|
| 181 |
+
// ============================================================================
|
| 182 |
+
|
| 183 |
+
#[inline]
|
| 184 |
+
fn overlap_minutes(a: &Shift, b: &Shift) -> i64 {
|
| 185 |
+
let start = a.start.max(b.start);
|
| 186 |
+
let end = a.end.min(b.end);
|
| 187 |
+
if start < end {
|
| 188 |
+
(end - start).num_minutes()
|
| 189 |
+
} else {
|
| 190 |
+
0
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
#[inline]
|
| 195 |
+
fn gap_penalty_minutes(a: &Shift, b: &Shift) -> i64 {
|
| 196 |
+
const MIN_GAP_MINUTES: i64 = 600;
|
| 197 |
+
|
| 198 |
+
let (earlier, later) = if a.end <= b.start {
|
| 199 |
+
(a, b)
|
| 200 |
+
} else if b.end <= a.start {
|
| 201 |
+
(b, a)
|
| 202 |
+
} else {
|
| 203 |
+
return 0;
|
| 204 |
+
};
|
| 205 |
+
|
| 206 |
+
let gap = (later.start - earlier.end).num_minutes();
|
| 207 |
+
if (0..MIN_GAP_MINUTES).contains(&gap) {
|
| 208 |
+
MIN_GAP_MINUTES - gap
|
| 209 |
+
} else {
|
| 210 |
+
0
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
#[inline]
|
| 215 |
+
fn shift_date_overlap_minutes(shift: &Shift, date: NaiveDate) -> i64 {
|
| 216 |
+
let day_start = date.and_hms_opt(0, 0, 0).unwrap();
|
| 217 |
+
let day_end = date.succ_opt().unwrap_or(date).and_hms_opt(0, 0, 0).unwrap();
|
| 218 |
+
|
| 219 |
+
let start = shift.start.max(day_start);
|
| 220 |
+
let end = shift.end.min(day_end);
|
| 221 |
+
|
| 222 |
+
if start < end {
|
| 223 |
+
(end - start).num_minutes()
|
| 224 |
+
} else {
|
| 225 |
+
0
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
src/demo_data.rs
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Demo data generators for Employee Scheduling.
|
| 2 |
+
|
| 3 |
+
use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Weekday};
|
| 4 |
+
use rand::prelude::*;
|
| 5 |
+
use rand::rngs::StdRng;
|
| 6 |
+
use rand::SeedableRng;
|
| 7 |
+
|
| 8 |
+
use crate::domain::{Employee, EmployeeSchedule, Shift};
|
| 9 |
+
|
| 10 |
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 11 |
+
pub enum DemoData {
|
| 12 |
+
Small,
|
| 13 |
+
Large,
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
impl std::str::FromStr for DemoData {
|
| 17 |
+
type Err = ();
|
| 18 |
+
|
| 19 |
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
| 20 |
+
match s.to_uppercase().as_str() {
|
| 21 |
+
"SMALL" => Ok(DemoData::Small),
|
| 22 |
+
"LARGE" => Ok(DemoData::Large),
|
| 23 |
+
_ => Err(()),
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
impl DemoData {
|
| 29 |
+
pub fn as_str(&self) -> &'static str {
|
| 30 |
+
match self {
|
| 31 |
+
DemoData::Small => "SMALL",
|
| 32 |
+
DemoData::Large => "LARGE",
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
fn parameters(&self) -> DemoDataParameters {
|
| 37 |
+
match self {
|
| 38 |
+
DemoData::Small => DemoDataParameters {
|
| 39 |
+
locations: vec![
|
| 40 |
+
"Ambulatory care".to_string(),
|
| 41 |
+
"Critical care".to_string(),
|
| 42 |
+
"Pediatric care".to_string(),
|
| 43 |
+
],
|
| 44 |
+
required_skills: vec!["Doctor".to_string(), "Nurse".to_string()],
|
| 45 |
+
optional_skills: vec!["Anaesthetics".to_string(), "Cardiology".to_string()],
|
| 46 |
+
days_in_schedule: 14,
|
| 47 |
+
employee_count: 15,
|
| 48 |
+
optional_skill_distribution: vec![(1, 3.0), (2, 1.0)],
|
| 49 |
+
shift_count_distribution: vec![(1, 0.9), (2, 0.1)],
|
| 50 |
+
availability_count_distribution: vec![(1, 4.0), (2, 3.0), (3, 2.0), (4, 1.0)],
|
| 51 |
+
},
|
| 52 |
+
DemoData::Large => DemoDataParameters {
|
| 53 |
+
locations: vec![
|
| 54 |
+
"Ambulatory care".to_string(),
|
| 55 |
+
"Neurology".to_string(),
|
| 56 |
+
"Critical care".to_string(),
|
| 57 |
+
"Pediatric care".to_string(),
|
| 58 |
+
"Surgery".to_string(),
|
| 59 |
+
"Radiology".to_string(),
|
| 60 |
+
"Outpatient".to_string(),
|
| 61 |
+
],
|
| 62 |
+
required_skills: vec!["Doctor".to_string(), "Nurse".to_string()],
|
| 63 |
+
optional_skills: vec![
|
| 64 |
+
"Anaesthetics".to_string(),
|
| 65 |
+
"Cardiology".to_string(),
|
| 66 |
+
"Radiology".to_string(),
|
| 67 |
+
],
|
| 68 |
+
days_in_schedule: 28,
|
| 69 |
+
employee_count: 50,
|
| 70 |
+
optional_skill_distribution: vec![(1, 3.0), (2, 1.0)],
|
| 71 |
+
shift_count_distribution: vec![(1, 0.5), (2, 0.3), (3, 0.2)],
|
| 72 |
+
availability_count_distribution: vec![(5, 4.0), (10, 3.0), (15, 2.0), (20, 1.0)],
|
| 73 |
+
},
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
struct DemoDataParameters {
|
| 79 |
+
locations: Vec<String>,
|
| 80 |
+
required_skills: Vec<String>,
|
| 81 |
+
optional_skills: Vec<String>,
|
| 82 |
+
days_in_schedule: i64,
|
| 83 |
+
employee_count: usize,
|
| 84 |
+
optional_skill_distribution: Vec<(usize, f64)>,
|
| 85 |
+
shift_count_distribution: Vec<(usize, f64)>,
|
| 86 |
+
availability_count_distribution: Vec<(usize, f64)>,
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/// List of available demo data sets.
|
| 90 |
+
pub fn list_demo_data() -> Vec<&'static str> {
|
| 91 |
+
vec!["SMALL", "LARGE"]
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/// Generates a demo schedule for the given size.
|
| 95 |
+
pub fn generate(demo: DemoData) -> EmployeeSchedule {
|
| 96 |
+
let params = demo.parameters();
|
| 97 |
+
let mut rng = StdRng::seed_from_u64(0);
|
| 98 |
+
|
| 99 |
+
// First Monday from a reference date
|
| 100 |
+
let start_date = find_next_monday(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
|
| 101 |
+
|
| 102 |
+
// Build location -> shift start times map (cycling through templates)
|
| 103 |
+
let shift_start_times_combos: Vec<Vec<NaiveTime>> = vec![
|
| 104 |
+
vec![time(6, 0), time(14, 0)],
|
| 105 |
+
vec![time(6, 0), time(14, 0), time(22, 0)],
|
| 106 |
+
vec![time(6, 0), time(9, 0), time(14, 0), time(22, 0)],
|
| 107 |
+
];
|
| 108 |
+
|
| 109 |
+
let location_to_shift_times: Vec<(&String, &Vec<NaiveTime>)> = params
|
| 110 |
+
.locations
|
| 111 |
+
.iter()
|
| 112 |
+
.enumerate()
|
| 113 |
+
.map(|(i, loc)| {
|
| 114 |
+
(
|
| 115 |
+
loc,
|
| 116 |
+
&shift_start_times_combos[i % shift_start_times_combos.len()],
|
| 117 |
+
)
|
| 118 |
+
})
|
| 119 |
+
.collect();
|
| 120 |
+
|
| 121 |
+
// Generate employee names (FIRST × LAST)
|
| 122 |
+
let name_permutations = generate_name_permutations(&mut rng);
|
| 123 |
+
|
| 124 |
+
// Generate employees
|
| 125 |
+
let mut employees = Vec::new();
|
| 126 |
+
for i in 0..params.employee_count {
|
| 127 |
+
let name = name_permutations[i % name_permutations.len()].clone();
|
| 128 |
+
|
| 129 |
+
// Pick optional skills based on distribution
|
| 130 |
+
let optional_count = pick_count(&mut rng, ¶ms.optional_skill_distribution);
|
| 131 |
+
let mut skills: Vec<String> = params
|
| 132 |
+
.optional_skills
|
| 133 |
+
.choose_multiple(&mut rng, optional_count.min(params.optional_skills.len()))
|
| 134 |
+
.cloned()
|
| 135 |
+
.collect();
|
| 136 |
+
|
| 137 |
+
// Add one required skill
|
| 138 |
+
if let Some(required) = params.required_skills.choose(&mut rng) {
|
| 139 |
+
skills.push(required.clone());
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
employees.push(Employee::new(i, &name).with_skills(skills));
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Generate shifts and assign availabilities
|
| 146 |
+
let mut shifts = Vec::new();
|
| 147 |
+
let mut shift_id = 0usize;
|
| 148 |
+
|
| 149 |
+
for day in 0..params.days_in_schedule {
|
| 150 |
+
let date = start_date + Duration::days(day);
|
| 151 |
+
|
| 152 |
+
// Pick employees to have availability entries on this day
|
| 153 |
+
let availability_count = pick_count(&mut rng, ¶ms.availability_count_distribution);
|
| 154 |
+
let employees_with_availability: Vec<usize> = (0..params.employee_count)
|
| 155 |
+
.collect::<Vec<_>>()
|
| 156 |
+
.choose_multiple(&mut rng, availability_count.min(params.employee_count))
|
| 157 |
+
.copied()
|
| 158 |
+
.collect();
|
| 159 |
+
|
| 160 |
+
for emp_idx in employees_with_availability {
|
| 161 |
+
match rng.gen_range(0..3) {
|
| 162 |
+
0 => {
|
| 163 |
+
employees[emp_idx].unavailable_dates.insert(date);
|
| 164 |
+
}
|
| 165 |
+
1 => {
|
| 166 |
+
employees[emp_idx].undesired_dates.insert(date);
|
| 167 |
+
}
|
| 168 |
+
2 => {
|
| 169 |
+
employees[emp_idx].desired_dates.insert(date);
|
| 170 |
+
}
|
| 171 |
+
_ => {}
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// Generate shifts for each location/timeslot
|
| 176 |
+
for (location, shift_times) in &location_to_shift_times {
|
| 177 |
+
for &shift_start in *shift_times {
|
| 178 |
+
let start = NaiveDateTime::new(date, shift_start);
|
| 179 |
+
let end = start + Duration::hours(8);
|
| 180 |
+
|
| 181 |
+
// How many shifts at this timeslot?
|
| 182 |
+
let shift_count = pick_count(&mut rng, ¶ms.shift_count_distribution);
|
| 183 |
+
|
| 184 |
+
for _ in 0..shift_count {
|
| 185 |
+
// Pick required skill (50% required, 50% optional)
|
| 186 |
+
let required_skill = if rng.gen_bool(0.5) {
|
| 187 |
+
params.required_skills.choose(&mut rng)
|
| 188 |
+
} else {
|
| 189 |
+
params.optional_skills.choose(&mut rng)
|
| 190 |
+
}
|
| 191 |
+
.cloned()
|
| 192 |
+
.unwrap_or_else(|| "Doctor".to_string());
|
| 193 |
+
|
| 194 |
+
shifts.push(Shift::new(
|
| 195 |
+
shift_id.to_string(),
|
| 196 |
+
start,
|
| 197 |
+
end,
|
| 198 |
+
(*location).clone(),
|
| 199 |
+
required_skill,
|
| 200 |
+
));
|
| 201 |
+
shift_id += 1;
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// Finalize employees to populate derived Vec fields
|
| 208 |
+
for emp in &mut employees {
|
| 209 |
+
emp.finalize();
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
EmployeeSchedule::new(employees, shifts)
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
fn time(hour: u32, minute: u32) -> NaiveTime {
|
| 216 |
+
NaiveTime::from_hms_opt(hour, minute, 0).unwrap()
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
fn find_next_monday(date: NaiveDate) -> NaiveDate {
|
| 220 |
+
let days_until_monday = match date.weekday() {
|
| 221 |
+
Weekday::Mon => 0,
|
| 222 |
+
Weekday::Tue => 6,
|
| 223 |
+
Weekday::Wed => 5,
|
| 224 |
+
Weekday::Thu => 4,
|
| 225 |
+
Weekday::Fri => 3,
|
| 226 |
+
Weekday::Sat => 2,
|
| 227 |
+
Weekday::Sun => 1,
|
| 228 |
+
};
|
| 229 |
+
date + Duration::days(days_until_monday)
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/// Pick a count based on weighted distribution.
|
| 233 |
+
fn pick_count(rng: &mut StdRng, distribution: &[(usize, f64)]) -> usize {
|
| 234 |
+
let total_weight: f64 = distribution.iter().map(|(_, w)| w).sum();
|
| 235 |
+
let mut choice = rng.gen::<f64>() * total_weight;
|
| 236 |
+
|
| 237 |
+
for (count, weight) in distribution {
|
| 238 |
+
if choice < *weight {
|
| 239 |
+
return *count;
|
| 240 |
+
}
|
| 241 |
+
choice -= weight;
|
| 242 |
+
}
|
| 243 |
+
distribution.last().map(|(c, _)| *c).unwrap_or(1)
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
const FIRST_NAMES: &[&str] = &[
|
| 247 |
+
"Amy", "Beth", "Carl", "Dan", "Elsa", "Flo", "Gus", "Hugo", "Ivy", "Jay",
|
| 248 |
+
];
|
| 249 |
+
const LAST_NAMES: &[&str] = &[
|
| 250 |
+
"Cole", "Fox", "Green", "Jones", "King", "Li", "Poe", "Rye", "Smith", "Watt",
|
| 251 |
+
];
|
| 252 |
+
|
| 253 |
+
fn generate_name_permutations(rng: &mut StdRng) -> Vec<String> {
|
| 254 |
+
let mut names = Vec::with_capacity(FIRST_NAMES.len() * LAST_NAMES.len());
|
| 255 |
+
for first in FIRST_NAMES {
|
| 256 |
+
for last in LAST_NAMES {
|
| 257 |
+
names.push(format!("{} {}", first, last));
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
names.shuffle(rng);
|
| 261 |
+
names
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
#[cfg(test)]
|
| 265 |
+
mod tests {
|
| 266 |
+
use super::*;
|
| 267 |
+
|
| 268 |
+
#[test]
|
| 269 |
+
fn test_generate_small() {
|
| 270 |
+
let schedule = generate(DemoData::Small);
|
| 271 |
+
|
| 272 |
+
assert_eq!(schedule.employees.len(), 15);
|
| 273 |
+
// 14 days × 3 locations × varying timeslots × varying shifts per timeslot
|
| 274 |
+
// Should be roughly 14 * 3 * avg(2,3,4) * avg(1,2) ≈ 14 * 3 * 3 * 1.1 ≈ 139
|
| 275 |
+
assert!(
|
| 276 |
+
schedule.shifts.len() >= 100,
|
| 277 |
+
"Expected >= 100 shifts, got {}",
|
| 278 |
+
schedule.shifts.len()
|
| 279 |
+
);
|
| 280 |
+
|
| 281 |
+
// All shifts should be unassigned initially
|
| 282 |
+
assert!(schedule.shifts.iter().all(|s| s.employee_idx.is_none()));
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
#[test]
|
| 286 |
+
fn test_generate_large() {
|
| 287 |
+
let schedule = generate(DemoData::Large);
|
| 288 |
+
|
| 289 |
+
assert_eq!(schedule.employees.len(), 50);
|
| 290 |
+
// 28 days × 7 locations × varying timeslots × varying shifts per timeslot
|
| 291 |
+
assert!(
|
| 292 |
+
schedule.shifts.len() >= 500,
|
| 293 |
+
"Expected >= 500 shifts, got {}",
|
| 294 |
+
schedule.shifts.len()
|
| 295 |
+
);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
#[test]
|
| 299 |
+
fn test_employees_have_skills() {
|
| 300 |
+
let schedule = generate(DemoData::Small);
|
| 301 |
+
|
| 302 |
+
for employee in &schedule.employees {
|
| 303 |
+
assert!(
|
| 304 |
+
!employee.skills.is_empty(),
|
| 305 |
+
"Employee {} has no skills",
|
| 306 |
+
employee.name
|
| 307 |
+
);
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
#[test]
|
| 312 |
+
fn test_demo_data_from_str() {
|
| 313 |
+
assert_eq!("SMALL".parse::<DemoData>(), Ok(DemoData::Small));
|
| 314 |
+
assert_eq!("small".parse::<DemoData>(), Ok(DemoData::Small));
|
| 315 |
+
assert_eq!("LARGE".parse::<DemoData>(), Ok(DemoData::Large));
|
| 316 |
+
assert!("invalid".parse::<DemoData>().is_err());
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
#[test]
|
| 320 |
+
fn test_medical_domain() {
|
| 321 |
+
let schedule = generate(DemoData::Small);
|
| 322 |
+
|
| 323 |
+
// Check for medical skills
|
| 324 |
+
let all_skills: std::collections::HashSet<_> = schedule
|
| 325 |
+
.employees
|
| 326 |
+
.iter()
|
| 327 |
+
.flat_map(|e| e.skills.iter())
|
| 328 |
+
.collect();
|
| 329 |
+
|
| 330 |
+
assert!(
|
| 331 |
+
all_skills.iter().any(|s| *s == "Doctor" || *s == "Nurse"),
|
| 332 |
+
"Should have Doctor or Nurse skills"
|
| 333 |
+
);
|
| 334 |
+
|
| 335 |
+
// Check for medical locations
|
| 336 |
+
let locations: std::collections::HashSet<_> = schedule
|
| 337 |
+
.shifts
|
| 338 |
+
.iter()
|
| 339 |
+
.map(|s| s.location.as_str())
|
| 340 |
+
.collect();
|
| 341 |
+
|
| 342 |
+
assert!(
|
| 343 |
+
locations.contains("Ambulatory care") || locations.contains("Critical care"),
|
| 344 |
+
"Should have medical locations"
|
| 345 |
+
);
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
#[test]
|
| 349 |
+
fn test_empty_schedule_has_score() {
|
| 350 |
+
use crate::domain::EmployeeSchedule;
|
| 351 |
+
use solverforge::Solvable;
|
| 352 |
+
use tokio::sync::mpsc::unbounded_channel;
|
| 353 |
+
|
| 354 |
+
// Empty schedule with no shifts and no employees
|
| 355 |
+
let schedule = EmployeeSchedule::new(vec![], vec![]);
|
| 356 |
+
let (sender, mut receiver) = unbounded_channel();
|
| 357 |
+
schedule.solve(None, sender);
|
| 358 |
+
|
| 359 |
+
// Try to receive solution - with 0 entities, solver may close channel without sending
|
| 360 |
+
if let Some((result, _score)) = receiver.blocking_recv() {
|
| 361 |
+
assert!(
|
| 362 |
+
result.score.is_some(),
|
| 363 |
+
"Empty schedule should have a score after solving, got None"
|
| 364 |
+
);
|
| 365 |
+
} else {
|
| 366 |
+
// If no solution was sent (channel closed), that's acceptable for 0 entities
|
| 367 |
+
// The solver may optimize this case by not running at all
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
}
|
src/domain.rs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Domain model for Employee Scheduling Problem.
|
| 2 |
+
|
| 3 |
+
use chrono::{NaiveDate, NaiveDateTime};
|
| 4 |
+
use serde::{Deserialize, Serialize};
|
| 5 |
+
use solverforge::prelude::*;
|
| 6 |
+
use std::collections::HashSet;
|
| 7 |
+
|
| 8 |
+
/// An employee who can be assigned to shifts.
|
| 9 |
+
#[problem_fact]
|
| 10 |
+
#[derive(Serialize, Deserialize)]
|
| 11 |
+
pub struct Employee {
|
| 12 |
+
/// Index of this employee in `EmployeeSchedule.employees` for O(1) join matching.
|
| 13 |
+
pub index: usize,
|
| 14 |
+
pub name: String,
|
| 15 |
+
pub skills: HashSet<String>,
|
| 16 |
+
#[serde(rename = "unavailableDates", default)]
|
| 17 |
+
pub unavailable_dates: HashSet<NaiveDate>,
|
| 18 |
+
#[serde(rename = "undesiredDates", default)]
|
| 19 |
+
pub undesired_dates: HashSet<NaiveDate>,
|
| 20 |
+
#[serde(rename = "desiredDates", default)]
|
| 21 |
+
pub desired_dates: HashSet<NaiveDate>,
|
| 22 |
+
/// Sorted unavailable dates for `flatten_last` compatibility.
|
| 23 |
+
/// Populated by `finalize()` from `unavailable_dates` HashSet.
|
| 24 |
+
#[serde(skip)]
|
| 25 |
+
pub unavailable_days: Vec<NaiveDate>,
|
| 26 |
+
/// Sorted undesired dates for `flatten_last` compatibility.
|
| 27 |
+
#[serde(skip)]
|
| 28 |
+
pub undesired_days: Vec<NaiveDate>,
|
| 29 |
+
/// Sorted desired dates for `flatten_last` compatibility.
|
| 30 |
+
#[serde(skip)]
|
| 31 |
+
pub desired_days: Vec<NaiveDate>,
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
impl Employee {
|
| 35 |
+
pub fn new(index: usize, name: impl Into<String>) -> Self {
|
| 36 |
+
Self {
|
| 37 |
+
index,
|
| 38 |
+
name: name.into(),
|
| 39 |
+
skills: HashSet::new(),
|
| 40 |
+
unavailable_dates: HashSet::new(),
|
| 41 |
+
undesired_dates: HashSet::new(),
|
| 42 |
+
desired_dates: HashSet::new(),
|
| 43 |
+
unavailable_days: Vec::new(),
|
| 44 |
+
undesired_days: Vec::new(),
|
| 45 |
+
desired_days: Vec::new(),
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/// Populates derived Vec fields from HashSets for zero-erasure stream compatibility.
|
| 50 |
+
/// Must be called after all dates have been added to HashSets.
|
| 51 |
+
pub fn finalize(&mut self) {
|
| 52 |
+
self.unavailable_days = self.unavailable_dates.iter().copied().collect();
|
| 53 |
+
self.unavailable_days.sort();
|
| 54 |
+
self.undesired_days = self.undesired_dates.iter().copied().collect();
|
| 55 |
+
self.undesired_days.sort();
|
| 56 |
+
self.desired_days = self.desired_dates.iter().copied().collect();
|
| 57 |
+
self.desired_days.sort();
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
pub fn with_skill(mut self, skill: impl Into<String>) -> Self {
|
| 61 |
+
self.skills.insert(skill.into());
|
| 62 |
+
self
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
pub fn with_skills(mut self, skills: impl IntoIterator<Item = impl Into<String>>) -> Self {
|
| 66 |
+
for skill in skills {
|
| 67 |
+
self.skills.insert(skill.into());
|
| 68 |
+
}
|
| 69 |
+
self
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
pub fn with_unavailable_date(mut self, date: NaiveDate) -> Self {
|
| 73 |
+
self.unavailable_dates.insert(date);
|
| 74 |
+
self
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
pub fn with_undesired_date(mut self, date: NaiveDate) -> Self {
|
| 78 |
+
self.undesired_dates.insert(date);
|
| 79 |
+
self
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
pub fn with_desired_date(mut self, date: NaiveDate) -> Self {
|
| 83 |
+
self.desired_dates.insert(date);
|
| 84 |
+
self
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/// A shift that needs to be staffed by an employee.
|
| 89 |
+
#[planning_entity]
|
| 90 |
+
#[derive(Serialize, Deserialize)]
|
| 91 |
+
pub struct Shift {
|
| 92 |
+
#[planning_id]
|
| 93 |
+
pub id: String,
|
| 94 |
+
pub start: NaiveDateTime,
|
| 95 |
+
pub end: NaiveDateTime,
|
| 96 |
+
pub location: String,
|
| 97 |
+
#[serde(rename = "requiredSkill")]
|
| 98 |
+
pub required_skill: String,
|
| 99 |
+
/// Index into `EmployeeSchedule.employees` (O(1) lookup, no String cloning).
|
| 100 |
+
#[planning_variable(allows_unassigned = true)]
|
| 101 |
+
pub employee_idx: Option<usize>,
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
impl Shift {
|
| 105 |
+
pub fn new(
|
| 106 |
+
id: impl Into<String>,
|
| 107 |
+
start: NaiveDateTime,
|
| 108 |
+
end: NaiveDateTime,
|
| 109 |
+
location: impl Into<String>,
|
| 110 |
+
required_skill: impl Into<String>,
|
| 111 |
+
) -> Self {
|
| 112 |
+
Self {
|
| 113 |
+
id: id.into(),
|
| 114 |
+
start,
|
| 115 |
+
end,
|
| 116 |
+
location: location.into(),
|
| 117 |
+
required_skill: required_skill.into(),
|
| 118 |
+
employee_idx: None,
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/// Returns the date of the shift start.
|
| 123 |
+
pub fn date(&self) -> NaiveDate {
|
| 124 |
+
self.start.date()
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/// Returns the duration in hours.
|
| 128 |
+
pub fn duration_hours(&self) -> f64 {
|
| 129 |
+
(self.end - self.start).num_minutes() as f64 / 60.0
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/// The employee scheduling solution.
|
| 134 |
+
#[planning_solution]
|
| 135 |
+
#[basic_variable_config(
|
| 136 |
+
entity_collection = "shifts",
|
| 137 |
+
variable_field = "employee_idx",
|
| 138 |
+
variable_type = "usize",
|
| 139 |
+
value_range = "employees"
|
| 140 |
+
)]
|
| 141 |
+
#[solverforge_constraints_path = "crate::constraints::create_fluent_constraints"]
|
| 142 |
+
#[derive(Serialize, Deserialize)]
|
| 143 |
+
pub struct EmployeeSchedule {
|
| 144 |
+
#[problem_fact_collection]
|
| 145 |
+
pub employees: Vec<Employee>,
|
| 146 |
+
#[planning_entity_collection]
|
| 147 |
+
pub shifts: Vec<Shift>,
|
| 148 |
+
#[planning_score]
|
| 149 |
+
pub score: Option<HardSoftDecimalScore>,
|
| 150 |
+
#[serde(rename = "solverStatus", skip_serializing_if = "Option::is_none")]
|
| 151 |
+
pub solver_status: Option<String>,
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
impl EmployeeSchedule {
|
| 155 |
+
pub fn new(employees: Vec<Employee>, shifts: Vec<Shift>) -> Self {
|
| 156 |
+
Self {
|
| 157 |
+
employees,
|
| 158 |
+
shifts,
|
| 159 |
+
score: None,
|
| 160 |
+
solver_status: None,
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
/// Gets an Employee by index (O(1)).
|
| 165 |
+
#[inline]
|
| 166 |
+
pub fn get_employee(&self, idx: usize) -> Option<&Employee> {
|
| 167 |
+
self.employees.get(idx)
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/// Returns the number of employees.
|
| 171 |
+
#[inline]
|
| 172 |
+
pub fn employee_count(&self) -> usize {
|
| 173 |
+
self.employees.len()
|
| 174 |
+
}
|
| 175 |
+
}
|
src/lib.rs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Employee Scheduling Quickstart for SolverForge
|
| 2 |
+
//!
|
| 3 |
+
//! This library provides the domain model, typed constraints, and solver for
|
| 4 |
+
//! employee scheduling optimization.
|
| 5 |
+
//!
|
| 6 |
+
//! Uses zero-erasure typed constraints via `TypedScoreDirector`.
|
| 7 |
+
|
| 8 |
+
pub mod api;
|
| 9 |
+
pub mod constraints;
|
| 10 |
+
pub mod demo_data;
|
| 11 |
+
pub mod domain;
|
src/main.rs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Employee Scheduling Quickstart - Axum Server
|
| 2 |
+
//!
|
| 3 |
+
//! Run with: cargo run -p employee-scheduling
|
| 4 |
+
//! Then open: http://localhost:7860
|
| 5 |
+
|
| 6 |
+
use std::net::SocketAddr;
|
| 7 |
+
use std::path::PathBuf;
|
| 8 |
+
use std::sync::Arc;
|
| 9 |
+
use tower_http::cors::{Any, CorsLayer};
|
| 10 |
+
use tower_http::services::ServeDir;
|
| 11 |
+
|
| 12 |
+
use employee_scheduling::api;
|
| 13 |
+
|
| 14 |
+
#[tokio::main]
|
| 15 |
+
async fn main() {
|
| 16 |
+
solverforge::console::init();
|
| 17 |
+
|
| 18 |
+
let state = Arc::new(api::AppState::new());
|
| 19 |
+
|
| 20 |
+
let cors = CorsLayer::new()
|
| 21 |
+
.allow_origin(Any)
|
| 22 |
+
.allow_methods(Any)
|
| 23 |
+
.allow_headers(Any);
|
| 24 |
+
|
| 25 |
+
let static_path = if PathBuf::from("examples/employee-scheduling/static").exists() {
|
| 26 |
+
"examples/employee-scheduling/static"
|
| 27 |
+
} else {
|
| 28 |
+
"static"
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
let app = api::router(state)
|
| 32 |
+
.fallback_service(ServeDir::new(static_path))
|
| 33 |
+
.layer(cors);
|
| 34 |
+
|
| 35 |
+
let addr = SocketAddr::from(([0, 0, 0, 0], 7860));
|
| 36 |
+
|
| 37 |
+
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
| 38 |
+
println!("Server running at http://localhost:{}", addr.port());
|
| 39 |
+
axum::serve(listener, app).await.unwrap();
|
| 40 |
+
}
|
src/solver.rs
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Solver service for Employee Scheduling.
|
| 2 |
+
//!
|
| 3 |
+
//! Uses Late Acceptance local search with change moves.
|
| 4 |
+
//! Incremental scoring via TypedScoreDirector for O(1) move evaluation.
|
| 5 |
+
|
| 6 |
+
use parking_lot::RwLock;
|
| 7 |
+
use rand::Rng;
|
| 8 |
+
use solverforge::prelude::*;
|
| 9 |
+
use solverforge::TypedScoreDirector;
|
| 10 |
+
use std::collections::HashMap;
|
| 11 |
+
use std::sync::Arc;
|
| 12 |
+
use std::time::{Duration, Instant};
|
| 13 |
+
use tokio::sync::oneshot;
|
| 14 |
+
use tracing::{debug, info};
|
| 15 |
+
|
| 16 |
+
use crate::console::{self, PhaseTimer};
|
| 17 |
+
use crate::constraints::create_fluent_constraints;
|
| 18 |
+
use crate::domain::EmployeeSchedule;
|
| 19 |
+
|
| 20 |
+
/// Default solving time: 30 seconds.
|
| 21 |
+
const DEFAULT_TIME_LIMIT_SECS: u64 = 30;
|
| 22 |
+
|
| 23 |
+
/// Late acceptance history size.
|
| 24 |
+
const LATE_ACCEPTANCE_SIZE: usize = 400;
|
| 25 |
+
|
| 26 |
+
/// Solver configuration with termination criteria.
|
| 27 |
+
#[derive(Debug, Clone, Default)]
|
| 28 |
+
pub struct SolverConfig {
|
| 29 |
+
/// Stop after this duration.
|
| 30 |
+
pub time_limit: Option<Duration>,
|
| 31 |
+
/// Stop after this duration without improvement.
|
| 32 |
+
pub unimproved_time_limit: Option<Duration>,
|
| 33 |
+
/// Stop after this many steps.
|
| 34 |
+
pub step_limit: Option<u64>,
|
| 35 |
+
/// Stop after this many steps without improvement.
|
| 36 |
+
pub unimproved_step_limit: Option<u64>,
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
impl SolverConfig {
|
| 40 |
+
/// Creates a config with default 30-second time limit.
|
| 41 |
+
pub fn default_config() -> Self {
|
| 42 |
+
Self {
|
| 43 |
+
time_limit: Some(Duration::from_secs(DEFAULT_TIME_LIMIT_SECS)),
|
| 44 |
+
..Default::default()
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/// Checks if any termination condition is met.
|
| 49 |
+
fn should_terminate(
|
| 50 |
+
&self,
|
| 51 |
+
elapsed: Duration,
|
| 52 |
+
steps: u64,
|
| 53 |
+
time_since_improvement: Duration,
|
| 54 |
+
steps_since_improvement: u64,
|
| 55 |
+
) -> bool {
|
| 56 |
+
if let Some(limit) = self.time_limit {
|
| 57 |
+
if elapsed >= limit {
|
| 58 |
+
return true;
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
if let Some(limit) = self.unimproved_time_limit {
|
| 62 |
+
if time_since_improvement >= limit {
|
| 63 |
+
return true;
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
if let Some(limit) = self.step_limit {
|
| 67 |
+
if steps >= limit {
|
| 68 |
+
return true;
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
if let Some(limit) = self.unimproved_step_limit {
|
| 72 |
+
if steps_since_improvement >= limit {
|
| 73 |
+
return true;
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
false
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/// Status of a solving job.
|
| 81 |
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
| 82 |
+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
| 83 |
+
pub enum SolverStatus {
|
| 84 |
+
/// Not currently solving.
|
| 85 |
+
NotSolving,
|
| 86 |
+
/// Actively solving.
|
| 87 |
+
Solving,
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
impl SolverStatus {
|
| 91 |
+
/// Returns the status as a SCREAMING_SNAKE_CASE string for API responses.
|
| 92 |
+
///
|
| 93 |
+
/// ```
|
| 94 |
+
/// use employee_scheduling::solver::SolverStatus;
|
| 95 |
+
///
|
| 96 |
+
/// assert_eq!(SolverStatus::NotSolving.as_str(), "NOT_SOLVING");
|
| 97 |
+
/// assert_eq!(SolverStatus::Solving.as_str(), "SOLVING");
|
| 98 |
+
/// ```
|
| 99 |
+
pub fn as_str(self) -> &'static str {
|
| 100 |
+
match self {
|
| 101 |
+
SolverStatus::NotSolving => "NOT_SOLVING",
|
| 102 |
+
SolverStatus::Solving => "SOLVING",
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/// A solving job with current state.
|
| 108 |
+
pub struct SolveJob {
|
| 109 |
+
/// Unique job identifier.
|
| 110 |
+
pub id: String,
|
| 111 |
+
/// Current status.
|
| 112 |
+
pub status: SolverStatus,
|
| 113 |
+
/// Current best schedule.
|
| 114 |
+
pub schedule: EmployeeSchedule,
|
| 115 |
+
/// Solver configuration.
|
| 116 |
+
pub config: SolverConfig,
|
| 117 |
+
/// Stop signal sender.
|
| 118 |
+
stop_signal: Option<oneshot::Sender<()>>,
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
impl SolveJob {
|
| 122 |
+
/// Creates a new solve job with default config.
|
| 123 |
+
pub fn new(id: String, schedule: EmployeeSchedule) -> Self {
|
| 124 |
+
Self {
|
| 125 |
+
id,
|
| 126 |
+
status: SolverStatus::NotSolving,
|
| 127 |
+
schedule,
|
| 128 |
+
config: SolverConfig::default_config(),
|
| 129 |
+
stop_signal: None,
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/// Creates a new solve job with custom config.
|
| 134 |
+
pub fn with_config(id: String, schedule: EmployeeSchedule, config: SolverConfig) -> Self {
|
| 135 |
+
Self {
|
| 136 |
+
id,
|
| 137 |
+
status: SolverStatus::NotSolving,
|
| 138 |
+
schedule,
|
| 139 |
+
config,
|
| 140 |
+
stop_signal: None,
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/// Manages Employee Scheduling solving jobs.
|
| 146 |
+
///
|
| 147 |
+
/// # Examples
|
| 148 |
+
///
|
| 149 |
+
/// ```
|
| 150 |
+
/// use employee_scheduling::solver::SolverService;
|
| 151 |
+
/// use employee_scheduling::demo_data::{generate, DemoData};
|
| 152 |
+
///
|
| 153 |
+
/// let service = SolverService::new();
|
| 154 |
+
/// let schedule = generate(DemoData::Small);
|
| 155 |
+
///
|
| 156 |
+
/// // Create a job (doesn't start solving yet)
|
| 157 |
+
/// let job = service.create_job("test-1".to_string(), schedule);
|
| 158 |
+
/// assert_eq!(job.read().status, employee_scheduling::solver::SolverStatus::NotSolving);
|
| 159 |
+
/// ```
|
| 160 |
+
pub struct SolverService {
|
| 161 |
+
jobs: RwLock<HashMap<String, Arc<RwLock<SolveJob>>>>,
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
impl SolverService {
|
| 165 |
+
/// Creates a new solver service.
|
| 166 |
+
pub fn new() -> Self {
|
| 167 |
+
Self {
|
| 168 |
+
jobs: RwLock::new(HashMap::new()),
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/// Creates a new job for the given schedule with default config.
|
| 173 |
+
pub fn create_job(&self, id: String, schedule: EmployeeSchedule) -> Arc<RwLock<SolveJob>> {
|
| 174 |
+
let job = Arc::new(RwLock::new(SolveJob::new(id.clone(), schedule)));
|
| 175 |
+
self.jobs.write().insert(id, job.clone());
|
| 176 |
+
job
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/// Creates a new job with custom config.
|
| 180 |
+
pub fn create_job_with_config(
|
| 181 |
+
&self,
|
| 182 |
+
id: String,
|
| 183 |
+
schedule: EmployeeSchedule,
|
| 184 |
+
config: SolverConfig,
|
| 185 |
+
) -> Arc<RwLock<SolveJob>> {
|
| 186 |
+
let job = Arc::new(RwLock::new(SolveJob::with_config(id.clone(), schedule, config)));
|
| 187 |
+
self.jobs.write().insert(id, job.clone());
|
| 188 |
+
job
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/// Gets a job by ID.
|
| 192 |
+
pub fn get_job(&self, id: &str) -> Option<Arc<RwLock<SolveJob>>> {
|
| 193 |
+
self.jobs.read().get(id).cloned()
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/// Lists all job IDs.
|
| 197 |
+
pub fn list_jobs(&self) -> Vec<String> {
|
| 198 |
+
self.jobs.read().keys().cloned().collect()
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
/// Removes a job by ID.
|
| 202 |
+
pub fn remove_job(&self, id: &str) -> Option<Arc<RwLock<SolveJob>>> {
|
| 203 |
+
self.jobs.write().remove(id)
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
/// Starts solving a job in the background.
|
| 207 |
+
pub fn start_solving(&self, job: Arc<RwLock<SolveJob>>) {
|
| 208 |
+
let (tx, rx) = oneshot::channel();
|
| 209 |
+
let config = job.read().config.clone();
|
| 210 |
+
|
| 211 |
+
{
|
| 212 |
+
let mut job_guard = job.write();
|
| 213 |
+
job_guard.status = SolverStatus::Solving;
|
| 214 |
+
job_guard.stop_signal = Some(tx);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
let job_clone = job.clone();
|
| 218 |
+
|
| 219 |
+
tokio::task::spawn_blocking(move || {
|
| 220 |
+
solve_blocking(job_clone, rx, config);
|
| 221 |
+
});
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
/// Stops a solving job.
|
| 225 |
+
pub fn stop_solving(&self, id: &str) -> bool {
|
| 226 |
+
if let Some(job) = self.get_job(id) {
|
| 227 |
+
let mut job_guard = job.write();
|
| 228 |
+
if let Some(stop_signal) = job_guard.stop_signal.take() {
|
| 229 |
+
let _ = stop_signal.send(());
|
| 230 |
+
job_guard.status = SolverStatus::NotSolving;
|
| 231 |
+
return true;
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
false
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
impl Default for SolverService {
|
| 239 |
+
fn default() -> Self {
|
| 240 |
+
Self::new()
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/// Runs the solver in a blocking context.
|
| 245 |
+
fn solve_blocking(
|
| 246 |
+
job: Arc<RwLock<SolveJob>>,
|
| 247 |
+
mut stop_rx: oneshot::Receiver<()>,
|
| 248 |
+
config: SolverConfig,
|
| 249 |
+
) {
|
| 250 |
+
let initial_schedule = job.read().schedule.clone();
|
| 251 |
+
let job_id = job.read().id.clone();
|
| 252 |
+
let solve_start = Instant::now();
|
| 253 |
+
|
| 254 |
+
// Print problem configuration
|
| 255 |
+
console::print_config(
|
| 256 |
+
initial_schedule.shifts.len(),
|
| 257 |
+
initial_schedule.employees.len(),
|
| 258 |
+
);
|
| 259 |
+
|
| 260 |
+
info!(
|
| 261 |
+
job_id = %job_id,
|
| 262 |
+
shifts = initial_schedule.shifts.len(),
|
| 263 |
+
employees = initial_schedule.employees.len(),
|
| 264 |
+
"Starting Employee Scheduling solver"
|
| 265 |
+
);
|
| 266 |
+
|
| 267 |
+
// Create typed constraints and score director
|
| 268 |
+
let constraints = create_fluent_constraints();
|
| 269 |
+
let mut director = TypedScoreDirector::new(initial_schedule.clone(), constraints);
|
| 270 |
+
|
| 271 |
+
// Phase 1: Construction heuristic (round-robin)
|
| 272 |
+
let mut ch_timer = PhaseTimer::start("ConstructionHeuristic", 0);
|
| 273 |
+
let mut current_score = construction_heuristic(&mut director, &mut ch_timer);
|
| 274 |
+
ch_timer.finish();
|
| 275 |
+
|
| 276 |
+
// Print solving started after construction
|
| 277 |
+
console::print_solving_started(
|
| 278 |
+
solve_start.elapsed().as_millis() as u64,
|
| 279 |
+
¤t_score.to_string(),
|
| 280 |
+
initial_schedule.shifts.len(),
|
| 281 |
+
initial_schedule.shifts.len(),
|
| 282 |
+
initial_schedule.employees.len(),
|
| 283 |
+
);
|
| 284 |
+
|
| 285 |
+
// Update job with constructed solution
|
| 286 |
+
update_job(&job, &director, current_score);
|
| 287 |
+
|
| 288 |
+
// Phase 2: Late Acceptance local search
|
| 289 |
+
let n_employees = director.working_solution().employees.len();
|
| 290 |
+
if n_employees == 0 {
|
| 291 |
+
info!("No employees to optimize");
|
| 292 |
+
console::print_solving_ended(
|
| 293 |
+
solve_start.elapsed(),
|
| 294 |
+
0,
|
| 295 |
+
1,
|
| 296 |
+
¤t_score.to_string(),
|
| 297 |
+
current_score.is_feasible(),
|
| 298 |
+
);
|
| 299 |
+
finish_job(&job, &director, current_score);
|
| 300 |
+
return;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
let mut ls_timer = PhaseTimer::start("LateAcceptance", 1);
|
| 304 |
+
let mut late_scores = vec![current_score; LATE_ACCEPTANCE_SIZE];
|
| 305 |
+
let mut step: u64 = 0;
|
| 306 |
+
let mut rng = rand::thread_rng();
|
| 307 |
+
|
| 308 |
+
// Track best score and improvement times
|
| 309 |
+
let mut best_score = current_score;
|
| 310 |
+
let mut last_improvement_time = solve_start;
|
| 311 |
+
let mut last_improvement_step: u64 = 0;
|
| 312 |
+
|
| 313 |
+
loop {
|
| 314 |
+
// Check termination conditions
|
| 315 |
+
let elapsed = solve_start.elapsed();
|
| 316 |
+
let time_since_improvement = last_improvement_time.elapsed();
|
| 317 |
+
let steps_since_improvement = step - last_improvement_step;
|
| 318 |
+
|
| 319 |
+
if config.should_terminate(elapsed, step, time_since_improvement, steps_since_improvement) {
|
| 320 |
+
debug!("Termination condition met");
|
| 321 |
+
break;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
// Check for stop signal
|
| 325 |
+
if stop_rx.try_recv().is_ok() {
|
| 326 |
+
info!("Solving terminated early by user");
|
| 327 |
+
break;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// Generate random change move
|
| 331 |
+
if let Some((shift_idx, new_employee_idx)) = generate_move(&director, &mut rng) {
|
| 332 |
+
ls_timer.record_move();
|
| 333 |
+
|
| 334 |
+
// Try the move
|
| 335 |
+
let old_score = current_score;
|
| 336 |
+
let old_employee_idx = apply_move(&mut director, shift_idx, new_employee_idx);
|
| 337 |
+
let new_score = director.get_score();
|
| 338 |
+
|
| 339 |
+
// Late acceptance criterion
|
| 340 |
+
let late_idx = (step as usize) % LATE_ACCEPTANCE_SIZE;
|
| 341 |
+
let late_score = late_scores[late_idx];
|
| 342 |
+
|
| 343 |
+
if new_score >= old_score || new_score >= late_score {
|
| 344 |
+
// Accept
|
| 345 |
+
ls_timer.record_accepted(¤t_score.to_string());
|
| 346 |
+
current_score = new_score;
|
| 347 |
+
late_scores[late_idx] = new_score;
|
| 348 |
+
|
| 349 |
+
// Track improvements
|
| 350 |
+
if new_score > best_score {
|
| 351 |
+
best_score = new_score;
|
| 352 |
+
last_improvement_time = Instant::now();
|
| 353 |
+
last_improvement_step = step;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
// Periodic update
|
| 357 |
+
if ls_timer.steps_accepted().is_multiple_of(1000) {
|
| 358 |
+
update_job(&job, &director, current_score);
|
| 359 |
+
debug!(
|
| 360 |
+
step,
|
| 361 |
+
moves_accepted = ls_timer.steps_accepted(),
|
| 362 |
+
score = %current_score,
|
| 363 |
+
elapsed_secs = solve_start.elapsed().as_secs(),
|
| 364 |
+
"Progress update"
|
| 365 |
+
);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
// Periodic console progress (every 10000 moves)
|
| 369 |
+
if ls_timer.moves_evaluated().is_multiple_of(10000) {
|
| 370 |
+
console::print_step_progress(
|
| 371 |
+
ls_timer.steps_accepted(),
|
| 372 |
+
ls_timer.elapsed(),
|
| 373 |
+
ls_timer.moves_evaluated(),
|
| 374 |
+
¤t_score.to_string(),
|
| 375 |
+
);
|
| 376 |
+
}
|
| 377 |
+
} else {
|
| 378 |
+
// Reject - undo
|
| 379 |
+
undo_move(&mut director, shift_idx, old_employee_idx);
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
step += 1;
|
| 383 |
+
}
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
ls_timer.finish();
|
| 387 |
+
|
| 388 |
+
let total_duration = solve_start.elapsed();
|
| 389 |
+
|
| 390 |
+
info!(
|
| 391 |
+
job_id = %job_id,
|
| 392 |
+
duration_secs = total_duration.as_secs_f64(),
|
| 393 |
+
steps = step,
|
| 394 |
+
score = %current_score,
|
| 395 |
+
feasible = current_score.is_feasible(),
|
| 396 |
+
"Solving complete"
|
| 397 |
+
);
|
| 398 |
+
|
| 399 |
+
console::print_solving_ended(
|
| 400 |
+
total_duration,
|
| 401 |
+
step,
|
| 402 |
+
2,
|
| 403 |
+
¤t_score.to_string(),
|
| 404 |
+
current_score.is_feasible(),
|
| 405 |
+
);
|
| 406 |
+
|
| 407 |
+
finish_job(&job, &director, current_score);
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
/// Construction heuristic: round-robin employee assignment.
|
| 411 |
+
fn construction_heuristic(
|
| 412 |
+
director: &mut TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
|
| 413 |
+
timer: &mut PhaseTimer,
|
| 414 |
+
) -> HardSoftDecimalScore {
|
| 415 |
+
// Initialize score
|
| 416 |
+
let _ = director.calculate_score();
|
| 417 |
+
|
| 418 |
+
let n_shifts = director.working_solution().shifts.len();
|
| 419 |
+
let n_employees = director.working_solution().employees.len();
|
| 420 |
+
|
| 421 |
+
if n_employees == 0 || n_shifts == 0 {
|
| 422 |
+
return director.get_score();
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
// Count already-assigned shifts
|
| 426 |
+
let assigned_count = director
|
| 427 |
+
.working_solution()
|
| 428 |
+
.shifts
|
| 429 |
+
.iter()
|
| 430 |
+
.filter(|s| s.employee_idx.is_some())
|
| 431 |
+
.count();
|
| 432 |
+
|
| 433 |
+
// If all shifts already assigned, skip construction
|
| 434 |
+
if assigned_count == n_shifts {
|
| 435 |
+
info!("All shifts already assigned, skipping construction heuristic");
|
| 436 |
+
return director.get_score();
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// Round-robin assignment for unassigned shifts only
|
| 440 |
+
let mut employee_idx = 0;
|
| 441 |
+
for shift_idx in 0..n_shifts {
|
| 442 |
+
if director.working_solution().shifts[shift_idx].employee_idx.is_some() {
|
| 443 |
+
continue;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
timer.record_move();
|
| 447 |
+
director.before_variable_changed(shift_idx);
|
| 448 |
+
director.working_solution_mut().shifts[shift_idx].employee_idx = Some(employee_idx);
|
| 449 |
+
director.after_variable_changed(shift_idx);
|
| 450 |
+
|
| 451 |
+
let score = director.get_score();
|
| 452 |
+
timer.record_accepted(&score.to_string());
|
| 453 |
+
|
| 454 |
+
employee_idx = (employee_idx + 1) % n_employees;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
director.get_score()
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
/// Generates a random change move (assign a different employee to a shift).
|
| 461 |
+
fn generate_move<R: Rng>(
|
| 462 |
+
director: &TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
|
| 463 |
+
rng: &mut R,
|
| 464 |
+
) -> Option<(usize, Option<usize>)> {
|
| 465 |
+
let solution = director.working_solution();
|
| 466 |
+
let n_shifts = solution.shifts.len();
|
| 467 |
+
let n_employees = solution.employees.len();
|
| 468 |
+
|
| 469 |
+
if n_shifts == 0 || n_employees == 0 {
|
| 470 |
+
return None;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
// Pick random shift
|
| 474 |
+
let shift_idx = rng.gen_range(0..n_shifts);
|
| 475 |
+
let current_employee = solution.shifts[shift_idx].employee_idx;
|
| 476 |
+
|
| 477 |
+
// Pick random new employee (different from current)
|
| 478 |
+
let new_employee_idx = rng.gen_range(0..n_employees);
|
| 479 |
+
|
| 480 |
+
// Skip no-op moves
|
| 481 |
+
if current_employee == Some(new_employee_idx) {
|
| 482 |
+
return None;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
Some((shift_idx, Some(new_employee_idx)))
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
/// Applies a change move, returns the old employee index.
|
| 489 |
+
fn apply_move(
|
| 490 |
+
director: &mut TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
|
| 491 |
+
shift_idx: usize,
|
| 492 |
+
new_employee_idx: Option<usize>,
|
| 493 |
+
) -> Option<usize> {
|
| 494 |
+
let old_employee_idx = director.working_solution().shifts[shift_idx].employee_idx;
|
| 495 |
+
|
| 496 |
+
director.before_variable_changed(shift_idx);
|
| 497 |
+
director.working_solution_mut().shifts[shift_idx].employee_idx = new_employee_idx;
|
| 498 |
+
director.after_variable_changed(shift_idx);
|
| 499 |
+
|
| 500 |
+
old_employee_idx
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
/// Undoes a change move.
|
| 504 |
+
fn undo_move(
|
| 505 |
+
director: &mut TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
|
| 506 |
+
shift_idx: usize,
|
| 507 |
+
old_employee_idx: Option<usize>,
|
| 508 |
+
) {
|
| 509 |
+
director.before_variable_changed(shift_idx);
|
| 510 |
+
director.working_solution_mut().shifts[shift_idx].employee_idx = old_employee_idx;
|
| 511 |
+
director.after_variable_changed(shift_idx);
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
/// Updates job with current solution.
|
| 515 |
+
fn update_job(
|
| 516 |
+
job: &Arc<RwLock<SolveJob>>,
|
| 517 |
+
director: &TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
|
| 518 |
+
score: HardSoftDecimalScore,
|
| 519 |
+
) {
|
| 520 |
+
let mut job_guard = job.write();
|
| 521 |
+
job_guard.schedule = director.clone_working_solution();
|
| 522 |
+
job_guard.schedule.score = Some(score);
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
/// Finishes job and sets status.
|
| 526 |
+
fn finish_job(
|
| 527 |
+
job: &Arc<RwLock<SolveJob>>,
|
| 528 |
+
director: &TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
|
| 529 |
+
score: HardSoftDecimalScore,
|
| 530 |
+
) {
|
| 531 |
+
let mut job_guard = job.write();
|
| 532 |
+
job_guard.schedule = director.clone_working_solution();
|
| 533 |
+
job_guard.schedule.score = Some(score);
|
| 534 |
+
job_guard.status = SolverStatus::NotSolving;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
#[cfg(test)]
|
| 538 |
+
mod tests {
|
| 539 |
+
use super::*;
|
| 540 |
+
use crate::demo_data::{generate, DemoData};
|
| 541 |
+
|
| 542 |
+
#[test]
|
| 543 |
+
fn test_construction_heuristic() {
|
| 544 |
+
let schedule = generate(DemoData::Small);
|
| 545 |
+
let constraints = create_fluent_constraints();
|
| 546 |
+
let mut director = TypedScoreDirector::new(schedule, constraints);
|
| 547 |
+
|
| 548 |
+
let mut timer = PhaseTimer::start("ConstructionHeuristic", 0);
|
| 549 |
+
let score = construction_heuristic(&mut director, &mut timer);
|
| 550 |
+
|
| 551 |
+
// All shifts should be assigned
|
| 552 |
+
let assigned_count = director
|
| 553 |
+
.working_solution()
|
| 554 |
+
.shifts
|
| 555 |
+
.iter()
|
| 556 |
+
.filter(|s| s.employee_idx.is_some())
|
| 557 |
+
.count();
|
| 558 |
+
let total_shifts = director.working_solution().shifts.len();
|
| 559 |
+
assert_eq!(assigned_count, total_shifts);
|
| 560 |
+
assert!(score.hard_scaled() <= 0); // May have some violations
|
| 561 |
+
}
|
| 562 |
+
}
|
static/app.js
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
let autoRefreshIntervalId = null;
|
| 2 |
+
const zoomMin = 2 * 1000 * 60 * 60 * 24 // 2 day in milliseconds
|
| 3 |
+
const zoomMax = 4 * 7 * 1000 * 60 * 60 * 24 // 4 weeks in milliseconds
|
| 4 |
+
|
| 5 |
+
const UNAVAILABLE_COLOR = '#ef2929' // Tango Scarlet Red
|
| 6 |
+
const UNDESIRED_COLOR = '#f57900' // Tango Orange
|
| 7 |
+
const DESIRED_COLOR = '#73d216' // Tango Chameleon
|
| 8 |
+
|
| 9 |
+
let demoDataId = null;
|
| 10 |
+
let scheduleId = null;
|
| 11 |
+
let loadedSchedule = null;
|
| 12 |
+
|
| 13 |
+
const byEmployeePanel = document.getElementById("byEmployeePanel");
|
| 14 |
+
const byEmployeeTimelineOptions = {
|
| 15 |
+
timeAxis: {scale: "hour", step: 6},
|
| 16 |
+
orientation: {axis: "top"},
|
| 17 |
+
stack: false,
|
| 18 |
+
xss: {disabled: true}, // Items are XSS safe through JQuery
|
| 19 |
+
zoomMin: zoomMin,
|
| 20 |
+
zoomMax: zoomMax,
|
| 21 |
+
};
|
| 22 |
+
let byEmployeeGroupDataSet = new vis.DataSet();
|
| 23 |
+
let byEmployeeItemDataSet = new vis.DataSet();
|
| 24 |
+
let byEmployeeTimeline = new vis.Timeline(byEmployeePanel, byEmployeeItemDataSet, byEmployeeGroupDataSet, byEmployeeTimelineOptions);
|
| 25 |
+
|
| 26 |
+
const byLocationPanel = document.getElementById("byLocationPanel");
|
| 27 |
+
const byLocationTimelineOptions = {
|
| 28 |
+
timeAxis: {scale: "hour", step: 6},
|
| 29 |
+
orientation: {axis: "top"},
|
| 30 |
+
xss: {disabled: true}, // Items are XSS safe through JQuery
|
| 31 |
+
zoomMin: zoomMin,
|
| 32 |
+
zoomMax: zoomMax,
|
| 33 |
+
};
|
| 34 |
+
let byLocationGroupDataSet = new vis.DataSet();
|
| 35 |
+
let byLocationItemDataSet = new vis.DataSet();
|
| 36 |
+
let byLocationTimeline = new vis.Timeline(byLocationPanel, byLocationItemDataSet, byLocationGroupDataSet, byLocationTimelineOptions);
|
| 37 |
+
|
| 38 |
+
let windowStart = JSJoda.LocalDate.now().toString();
|
| 39 |
+
let windowEnd = JSJoda.LocalDate.parse(windowStart).plusDays(7).toString();
|
| 40 |
+
|
| 41 |
+
$(document).ready(function () {
|
| 42 |
+
let initialized = false;
|
| 43 |
+
|
| 44 |
+
function safeInitialize() {
|
| 45 |
+
if (!initialized) {
|
| 46 |
+
initialized = true;
|
| 47 |
+
initializeApp();
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Ensure all resources are loaded before initializing
|
| 52 |
+
$(window).on('load', safeInitialize);
|
| 53 |
+
|
| 54 |
+
// Fallback if window load event doesn't fire
|
| 55 |
+
setTimeout(safeInitialize, 100);
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
function initializeApp() {
|
| 59 |
+
replaceQuickstartSolverForgeAutoHeaderFooter();
|
| 60 |
+
|
| 61 |
+
$("#solveButton").click(function () {
|
| 62 |
+
solve();
|
| 63 |
+
});
|
| 64 |
+
$("#stopSolvingButton").click(function () {
|
| 65 |
+
stopSolving();
|
| 66 |
+
});
|
| 67 |
+
$("#analyzeButton").click(function () {
|
| 68 |
+
analyze();
|
| 69 |
+
});
|
| 70 |
+
// HACK to allow vis-timeline to work within Bootstrap tabs
|
| 71 |
+
$("#byEmployeeTab").on('shown.bs.tab', function (event) {
|
| 72 |
+
byEmployeeTimeline.redraw();
|
| 73 |
+
})
|
| 74 |
+
$("#byLocationTab").on('shown.bs.tab', function (event) {
|
| 75 |
+
byLocationTimeline.redraw();
|
| 76 |
+
})
|
| 77 |
+
|
| 78 |
+
setupAjax();
|
| 79 |
+
fetchDemoData();
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
function setupAjax() {
|
| 83 |
+
$.ajaxSetup({
|
| 84 |
+
headers: {
|
| 85 |
+
'Content-Type': 'application/json',
|
| 86 |
+
'Accept': 'application/json,text/plain', // plain text is required by solve() returning UUID of the solver job
|
| 87 |
+
}
|
| 88 |
+
});
|
| 89 |
+
// Extend jQuery to support $.put() and $.delete()
|
| 90 |
+
jQuery.each(["put", "delete"], function (i, method) {
|
| 91 |
+
jQuery[method] = function (url, data, callback, type) {
|
| 92 |
+
if (jQuery.isFunction(data)) {
|
| 93 |
+
type = type || callback;
|
| 94 |
+
callback = data;
|
| 95 |
+
data = undefined;
|
| 96 |
+
}
|
| 97 |
+
return jQuery.ajax({
|
| 98 |
+
url: url,
|
| 99 |
+
type: method,
|
| 100 |
+
dataType: type,
|
| 101 |
+
data: data,
|
| 102 |
+
success: callback
|
| 103 |
+
});
|
| 104 |
+
};
|
| 105 |
+
});
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
function fetchDemoData() {
|
| 109 |
+
$.get("/demo-data", function (data) {
|
| 110 |
+
data.forEach(item => {
|
| 111 |
+
$("#testDataButton").append($('<a id="' + item + 'TestData" class="dropdown-item" href="#">' + item + '</a>'));
|
| 112 |
+
$("#" + item + "TestData").click(function () {
|
| 113 |
+
switchDataDropDownItemActive(item);
|
| 114 |
+
scheduleId = null;
|
| 115 |
+
demoDataId = item;
|
| 116 |
+
|
| 117 |
+
refreshSchedule();
|
| 118 |
+
});
|
| 119 |
+
});
|
| 120 |
+
demoDataId = data[0];
|
| 121 |
+
switchDataDropDownItemActive(demoDataId);
|
| 122 |
+
refreshSchedule();
|
| 123 |
+
}).fail(function (xhr, ajaxOptions, thrownError) {
|
| 124 |
+
// disable this page as there is no data
|
| 125 |
+
let $demo = $("#demo");
|
| 126 |
+
$demo.empty();
|
| 127 |
+
$demo.html("<h1><p align=\"center\">No test data available</p></h1>")
|
| 128 |
+
});
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
function switchDataDropDownItemActive(newItem) {
|
| 132 |
+
activeCssClass = "active";
|
| 133 |
+
$("#testDataButton > a." + activeCssClass).removeClass(activeCssClass);
|
| 134 |
+
$("#" + newItem + "TestData").addClass(activeCssClass);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
function getShiftColor(shift, employee) {
|
| 138 |
+
const shiftStart = JSJoda.LocalDateTime.parse(shift.start);
|
| 139 |
+
const shiftStartDateString = shiftStart.toLocalDate().toString();
|
| 140 |
+
const shiftEnd = JSJoda.LocalDateTime.parse(shift.end);
|
| 141 |
+
const shiftEndDateString = shiftEnd.toLocalDate().toString();
|
| 142 |
+
if (employee.unavailableDates.includes(shiftStartDateString) ||
|
| 143 |
+
// The contains() check is ignored for a shift end at midnight (00:00:00).
|
| 144 |
+
(shiftEnd.isAfter(shiftStart.toLocalDate().plusDays(1).atStartOfDay()) &&
|
| 145 |
+
employee.unavailableDates.includes(shiftEndDateString))) {
|
| 146 |
+
return UNAVAILABLE_COLOR
|
| 147 |
+
} else if (employee.undesiredDates.includes(shiftStartDateString) ||
|
| 148 |
+
// The contains() check is ignored for a shift end at midnight (00:00:00).
|
| 149 |
+
(shiftEnd.isAfter(shiftStart.toLocalDate().plusDays(1).atStartOfDay()) &&
|
| 150 |
+
employee.undesiredDates.includes(shiftEndDateString))) {
|
| 151 |
+
return UNDESIRED_COLOR
|
| 152 |
+
} else if (employee.desiredDates.includes(shiftStartDateString) ||
|
| 153 |
+
// The contains() check is ignored for a shift end at midnight (00:00:00).
|
| 154 |
+
(shiftEnd.isAfter(shiftStart.toLocalDate().plusDays(1).atStartOfDay()) &&
|
| 155 |
+
employee.desiredDates.includes(shiftEndDateString))) {
|
| 156 |
+
return DESIRED_COLOR
|
| 157 |
+
} else {
|
| 158 |
+
return " #729fcf"; // Tango Sky Blue
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
function refreshSchedule() {
|
| 163 |
+
let path = "/schedules/" + scheduleId;
|
| 164 |
+
if (scheduleId === null) {
|
| 165 |
+
if (demoDataId === null) {
|
| 166 |
+
alert("Please select a test data set.");
|
| 167 |
+
return;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
path = "/demo-data/" + demoDataId;
|
| 171 |
+
}
|
| 172 |
+
$.getJSON(path, function (schedule) {
|
| 173 |
+
loadedSchedule = schedule;
|
| 174 |
+
renderSchedule(schedule);
|
| 175 |
+
})
|
| 176 |
+
.fail(function (xhr, ajaxOptions, thrownError) {
|
| 177 |
+
showError("Getting the schedule has failed.", xhr);
|
| 178 |
+
refreshSolvingButtons(false);
|
| 179 |
+
});
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
function renderSchedule(schedule) {
|
| 183 |
+
console.log('Rendering schedule:', schedule);
|
| 184 |
+
|
| 185 |
+
if (!schedule) {
|
| 186 |
+
console.error('No schedule data provided to renderSchedule');
|
| 187 |
+
return;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
refreshSolvingButtons(schedule.solverStatus != null && schedule.solverStatus !== "NOT_SOLVING");
|
| 191 |
+
$("#score").text("Score: " + (schedule.score == null ? "?" : schedule.score));
|
| 192 |
+
|
| 193 |
+
const unassignedShifts = $("#unassignedShifts");
|
| 194 |
+
const groups = [];
|
| 195 |
+
|
| 196 |
+
// Check if schedule.shifts exists and is an array
|
| 197 |
+
if (!schedule.shifts || !Array.isArray(schedule.shifts) || schedule.shifts.length === 0) {
|
| 198 |
+
console.warn('No shifts data available in schedule');
|
| 199 |
+
return;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// Show only first 7 days of draft
|
| 203 |
+
const scheduleStart = schedule.shifts.map(shift => JSJoda.LocalDateTime.parse(shift.start).toLocalDate()).sort()[0].toString();
|
| 204 |
+
const scheduleEnd = JSJoda.LocalDate.parse(scheduleStart).plusDays(7).toString();
|
| 205 |
+
|
| 206 |
+
windowStart = scheduleStart;
|
| 207 |
+
windowEnd = scheduleEnd;
|
| 208 |
+
|
| 209 |
+
unassignedShifts.children().remove();
|
| 210 |
+
let unassignedShiftsCount = 0;
|
| 211 |
+
byEmployeeGroupDataSet.clear();
|
| 212 |
+
byLocationGroupDataSet.clear();
|
| 213 |
+
|
| 214 |
+
byEmployeeItemDataSet.clear();
|
| 215 |
+
byLocationItemDataSet.clear();
|
| 216 |
+
|
| 217 |
+
// Check if schedule.employees exists and is an array
|
| 218 |
+
if (!schedule.employees || !Array.isArray(schedule.employees)) {
|
| 219 |
+
console.warn('No employees data available in schedule');
|
| 220 |
+
return;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
schedule.employees.forEach((employee, index) => {
|
| 224 |
+
const employeeGroupElement = $('<div class="card-body p-2"/>')
|
| 225 |
+
.append($(`<h5 class="card-title mb-2"/>)`)
|
| 226 |
+
.append(employee.name))
|
| 227 |
+
.append($('<div/>')
|
| 228 |
+
.append($(employee.skills.map(skill => `<span class="badge me-1 mt-1" style="background-color:#d3d7cf">${skill}</span>`).join(''))));
|
| 229 |
+
byEmployeeGroupDataSet.add({id: employee.name, content: employeeGroupElement.html()});
|
| 230 |
+
|
| 231 |
+
employee.unavailableDates.forEach((rawDate, dateIndex) => {
|
| 232 |
+
const date = JSJoda.LocalDate.parse(rawDate)
|
| 233 |
+
const start = date.atStartOfDay().toString();
|
| 234 |
+
const end = date.plusDays(1).atStartOfDay().toString();
|
| 235 |
+
const byEmployeeShiftElement = $(`<div/>`)
|
| 236 |
+
.append($(`<h5 class="card-title mb-1"/>`).text("Unavailable"));
|
| 237 |
+
byEmployeeItemDataSet.add({
|
| 238 |
+
id: "employee-" + index + "-unavailability-" + dateIndex, group: employee.name,
|
| 239 |
+
content: byEmployeeShiftElement.html(),
|
| 240 |
+
start: start, end: end,
|
| 241 |
+
type: "background",
|
| 242 |
+
style: "opacity: 0.5; background-color: " + UNAVAILABLE_COLOR,
|
| 243 |
+
});
|
| 244 |
+
});
|
| 245 |
+
employee.undesiredDates.forEach((rawDate, dateIndex) => {
|
| 246 |
+
const date = JSJoda.LocalDate.parse(rawDate)
|
| 247 |
+
const start = date.atStartOfDay().toString();
|
| 248 |
+
const end = date.plusDays(1).atStartOfDay().toString();
|
| 249 |
+
const byEmployeeShiftElement = $(`<div/>`)
|
| 250 |
+
.append($(`<h5 class="card-title mb-1"/>`).text("Undesired"));
|
| 251 |
+
byEmployeeItemDataSet.add({
|
| 252 |
+
id: "employee-" + index + "-undesired-" + dateIndex, group: employee.name,
|
| 253 |
+
content: byEmployeeShiftElement.html(),
|
| 254 |
+
start: start, end: end,
|
| 255 |
+
type: "background",
|
| 256 |
+
style: "opacity: 0.5; background-color: " + UNDESIRED_COLOR,
|
| 257 |
+
});
|
| 258 |
+
});
|
| 259 |
+
employee.desiredDates.forEach((rawDate, dateIndex) => {
|
| 260 |
+
const date = JSJoda.LocalDate.parse(rawDate)
|
| 261 |
+
const start = date.atStartOfDay().toString();
|
| 262 |
+
const end = date.plusDays(1).atStartOfDay().toString();
|
| 263 |
+
const byEmployeeShiftElement = $(`<div/>`)
|
| 264 |
+
.append($(`<h5 class="card-title mb-1"/>`).text("Desired"));
|
| 265 |
+
byEmployeeItemDataSet.add({
|
| 266 |
+
id: "employee-" + index + "-desired-" + dateIndex, group: employee.name,
|
| 267 |
+
content: byEmployeeShiftElement.html(),
|
| 268 |
+
start: start, end: end,
|
| 269 |
+
type: "background",
|
| 270 |
+
style: "opacity: 0.5; background-color: " + DESIRED_COLOR,
|
| 271 |
+
});
|
| 272 |
+
});
|
| 273 |
+
});
|
| 274 |
+
|
| 275 |
+
schedule.shifts.forEach((shift, index) => {
|
| 276 |
+
if (groups.indexOf(shift.location) === -1) {
|
| 277 |
+
groups.push(shift.location);
|
| 278 |
+
byLocationGroupDataSet.add({
|
| 279 |
+
id: shift.location,
|
| 280 |
+
content: shift.location,
|
| 281 |
+
});
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
if (shift.employee == null) {
|
| 285 |
+
unassignedShiftsCount++;
|
| 286 |
+
|
| 287 |
+
const byLocationShiftElement = $('<div class="card-body p-2"/>')
|
| 288 |
+
.append($(`<h5 class="card-title mb-2"/>)`)
|
| 289 |
+
.append("Unassigned"))
|
| 290 |
+
.append($('<div/>')
|
| 291 |
+
.append($(`<span class="badge me-1 mt-1" style="background-color:#d3d7cf">${shift.requiredSkill}</span>`)));
|
| 292 |
+
|
| 293 |
+
byLocationItemDataSet.add({
|
| 294 |
+
id: 'shift-' + index, group: shift.location,
|
| 295 |
+
content: byLocationShiftElement.html(),
|
| 296 |
+
start: shift.start, end: shift.end,
|
| 297 |
+
style: "background-color: #EF292999"
|
| 298 |
+
});
|
| 299 |
+
} else {
|
| 300 |
+
const skillColor = (shift.employee.skills.indexOf(shift.requiredSkill) === -1 ? '#ef2929' : '#8ae234');
|
| 301 |
+
const byEmployeeShiftElement = $('<div class="card-body p-2"/>')
|
| 302 |
+
.append($(`<h5 class="card-title mb-2"/>)`)
|
| 303 |
+
.append(shift.location))
|
| 304 |
+
.append($('<div/>')
|
| 305 |
+
.append($(`<span class="badge me-1 mt-1" style="background-color:${skillColor}">${shift.requiredSkill}</span>`)));
|
| 306 |
+
const byLocationShiftElement = $('<div class="card-body p-2"/>')
|
| 307 |
+
.append($(`<h5 class="card-title mb-2"/>)`)
|
| 308 |
+
.append(shift.employee.name))
|
| 309 |
+
.append($('<div/>')
|
| 310 |
+
.append($(`<span class="badge me-1 mt-1" style="background-color:${skillColor}">${shift.requiredSkill}</span>`)));
|
| 311 |
+
|
| 312 |
+
const shiftColor = getShiftColor(shift, shift.employee);
|
| 313 |
+
byEmployeeItemDataSet.add({
|
| 314 |
+
id: 'shift-' + index, group: shift.employee.name,
|
| 315 |
+
content: byEmployeeShiftElement.html(),
|
| 316 |
+
start: shift.start, end: shift.end,
|
| 317 |
+
style: "background-color: " + shiftColor
|
| 318 |
+
});
|
| 319 |
+
byLocationItemDataSet.add({
|
| 320 |
+
id: 'shift-' + index, group: shift.location,
|
| 321 |
+
content: byLocationShiftElement.html(),
|
| 322 |
+
start: shift.start, end: shift.end,
|
| 323 |
+
style: "background-color: " + shiftColor
|
| 324 |
+
});
|
| 325 |
+
}
|
| 326 |
+
});
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
if (unassignedShiftsCount === 0) {
|
| 330 |
+
unassignedShifts.append($(`<p/>`).text(`There are no unassigned shifts.`));
|
| 331 |
+
} else {
|
| 332 |
+
unassignedShifts.append($(`<p/>`).text(`There are ${unassignedShiftsCount} unassigned shifts.`));
|
| 333 |
+
}
|
| 334 |
+
byEmployeeTimeline.setWindow(scheduleStart, scheduleEnd);
|
| 335 |
+
byLocationTimeline.setWindow(scheduleStart, scheduleEnd);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
function solve() {
|
| 339 |
+
if (!loadedSchedule) {
|
| 340 |
+
showError("No schedule data loaded. Please wait for the data to load or refresh the page.");
|
| 341 |
+
return;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
console.log('Sending schedule data for solving:', loadedSchedule);
|
| 345 |
+
$.post("/schedules", JSON.stringify(loadedSchedule), function (data) {
|
| 346 |
+
scheduleId = data;
|
| 347 |
+
refreshSolvingButtons(true);
|
| 348 |
+
}).fail(function (xhr, ajaxOptions, thrownError) {
|
| 349 |
+
showError("Start solving failed.", xhr);
|
| 350 |
+
refreshSolvingButtons(false);
|
| 351 |
+
},
|
| 352 |
+
"text");
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
function analyze() {
|
| 356 |
+
new bootstrap.Modal("#scoreAnalysisModal").show()
|
| 357 |
+
const scoreAnalysisModalContent = $("#scoreAnalysisModalContent");
|
| 358 |
+
scoreAnalysisModalContent.children().remove();
|
| 359 |
+
if (loadedSchedule.score == null) {
|
| 360 |
+
scoreAnalysisModalContent.text("No score to analyze yet, please first press the 'solve' button.");
|
| 361 |
+
} else {
|
| 362 |
+
$('#scoreAnalysisScoreLabel').text(`(${loadedSchedule.score})`);
|
| 363 |
+
$.put("/schedules/analyze", JSON.stringify(loadedSchedule), function (scoreAnalysis) {
|
| 364 |
+
let constraints = scoreAnalysis.constraints;
|
| 365 |
+
constraints.sort((a, b) => {
|
| 366 |
+
let aComponents = getScoreComponents(a.score), bComponents = getScoreComponents(b.score);
|
| 367 |
+
if (aComponents.hard < 0 && bComponents.hard > 0) return -1;
|
| 368 |
+
if (aComponents.hard > 0 && bComponents.soft < 0) return 1;
|
| 369 |
+
if (Math.abs(aComponents.hard) > Math.abs(bComponents.hard)) {
|
| 370 |
+
return -1;
|
| 371 |
+
} else {
|
| 372 |
+
if (aComponents.medium < 0 && bComponents.medium > 0) return -1;
|
| 373 |
+
if (aComponents.medium > 0 && bComponents.medium < 0) return 1;
|
| 374 |
+
if (Math.abs(aComponents.medium) > Math.abs(bComponents.medium)) {
|
| 375 |
+
return -1;
|
| 376 |
+
} else {
|
| 377 |
+
if (aComponents.soft < 0 && bComponents.soft > 0) return -1;
|
| 378 |
+
if (aComponents.soft > 0 && bComponents.soft < 0) return 1;
|
| 379 |
+
|
| 380 |
+
return Math.abs(bComponents.soft) - Math.abs(aComponents.soft);
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
});
|
| 384 |
+
constraints.map((e) => {
|
| 385 |
+
let components = getScoreComponents(e.weight);
|
| 386 |
+
e.type = components.hard != 0 ? 'hard' : (components.medium != 0 ? 'medium' : 'soft');
|
| 387 |
+
e.weight = components[e.type];
|
| 388 |
+
let scores = getScoreComponents(e.score);
|
| 389 |
+
e.implicitScore = scores.hard != 0 ? scores.hard : (scores.medium != 0 ? scores.medium : scores.soft);
|
| 390 |
+
});
|
| 391 |
+
scoreAnalysis.constraints = constraints;
|
| 392 |
+
|
| 393 |
+
scoreAnalysisModalContent.children().remove();
|
| 394 |
+
scoreAnalysisModalContent.text("");
|
| 395 |
+
|
| 396 |
+
const analysisTable = $(`<table class="table"/>`).css({textAlign: 'center'});
|
| 397 |
+
const analysisTHead = $(`<thead/>`).append($(`<tr/>`)
|
| 398 |
+
.append($(`<th></th>`))
|
| 399 |
+
.append($(`<th>Constraint</th>`).css({textAlign: 'left'}))
|
| 400 |
+
.append($(`<th>Type</th>`))
|
| 401 |
+
.append($(`<th># Matches</th>`))
|
| 402 |
+
.append($(`<th>Weight</th>`))
|
| 403 |
+
.append($(`<th>Score</th>`))
|
| 404 |
+
.append($(`<th></th>`)));
|
| 405 |
+
analysisTable.append(analysisTHead);
|
| 406 |
+
const analysisTBody = $(`<tbody/>`)
|
| 407 |
+
$.each(scoreAnalysis.constraints, (index, constraintAnalysis) => {
|
| 408 |
+
let icon = constraintAnalysis.type == "hard" && constraintAnalysis.implicitScore < 0 ? '<span class="fas fa-exclamation-triangle" style="color: red"></span>' : '';
|
| 409 |
+
if (!icon) icon = constraintAnalysis.matches.length == 0 ? '<span class="fas fa-check-circle" style="color: green"></span>' : '';
|
| 410 |
+
|
| 411 |
+
let row = $(`<tr/>`);
|
| 412 |
+
row.append($(`<td/>`).html(icon))
|
| 413 |
+
.append($(`<td/>`).text(constraintAnalysis.name).css({textAlign: 'left'}))
|
| 414 |
+
.append($(`<td/>`).text(constraintAnalysis.type))
|
| 415 |
+
.append($(`<td/>`).html(`<b>${constraintAnalysis.matches.length}</b>`))
|
| 416 |
+
.append($(`<td/>`).text(constraintAnalysis.weight))
|
| 417 |
+
.append($(`<td/>`).text(constraintAnalysis.implicitScore));
|
| 418 |
+
analysisTBody.append(row);
|
| 419 |
+
row.append($(`<td/>`));
|
| 420 |
+
});
|
| 421 |
+
analysisTable.append(analysisTBody);
|
| 422 |
+
scoreAnalysisModalContent.append(analysisTable);
|
| 423 |
+
}).fail(function (xhr, ajaxOptions, thrownError) {
|
| 424 |
+
showError("Analyze failed.", xhr);
|
| 425 |
+
}, "text");
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
function getScoreComponents(score) {
|
| 430 |
+
let components = {hard: 0, medium: 0, soft: 0};
|
| 431 |
+
|
| 432 |
+
$.each([...score.matchAll(/(-?\d*(\.\d+)?)(hard|medium|soft)/g)], (i, parts) => {
|
| 433 |
+
components[parts[3]] = parseFloat(parts[1], 10);
|
| 434 |
+
});
|
| 435 |
+
|
| 436 |
+
return components;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
function refreshSolvingButtons(solving) {
|
| 440 |
+
if (solving) {
|
| 441 |
+
$("#solveButton").hide();
|
| 442 |
+
$("#stopSolvingButton").show();
|
| 443 |
+
$("#solvingSpinner").addClass("active");
|
| 444 |
+
if (autoRefreshIntervalId == null) {
|
| 445 |
+
autoRefreshIntervalId = setInterval(refreshSchedule, 2000);
|
| 446 |
+
}
|
| 447 |
+
} else {
|
| 448 |
+
$("#solveButton").show();
|
| 449 |
+
$("#stopSolvingButton").hide();
|
| 450 |
+
$("#solvingSpinner").removeClass("active");
|
| 451 |
+
if (autoRefreshIntervalId != null) {
|
| 452 |
+
clearInterval(autoRefreshIntervalId);
|
| 453 |
+
autoRefreshIntervalId = null;
|
| 454 |
+
}
|
| 455 |
+
}
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
function stopSolving() {
|
| 459 |
+
$.delete(`/schedules/${scheduleId}`, function () {
|
| 460 |
+
refreshSolvingButtons(false);
|
| 461 |
+
refreshSchedule();
|
| 462 |
+
}).fail(function (xhr, ajaxOptions, thrownError) {
|
| 463 |
+
showError("Stop solving failed.", xhr);
|
| 464 |
+
});
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
function replaceQuickstartSolverForgeAutoHeaderFooter() {
|
| 468 |
+
const solverforgeHeader = $("header#solverforge-auto-header");
|
| 469 |
+
if (solverforgeHeader != null) {
|
| 470 |
+
solverforgeHeader.css("background-color", "#ffffff");
|
| 471 |
+
solverforgeHeader.append(
|
| 472 |
+
$(`<div class="container-fluid">
|
| 473 |
+
<nav class="navbar sticky-top navbar-expand-lg shadow-sm mb-3" style="background-color: #ffffff;">
|
| 474 |
+
<a class="navbar-brand" href="https://www.solverforge.org">
|
| 475 |
+
<img src="/webjars/solverforge/img/solverforge-horizontal.svg" alt="SolverForge logo" width="400">
|
| 476 |
+
</a>
|
| 477 |
+
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
| 478 |
+
<span class="navbar-toggler-icon"></span>
|
| 479 |
+
</button>
|
| 480 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 481 |
+
<ul class="nav nav-pills">
|
| 482 |
+
<li class="nav-item active" id="navUIItem">
|
| 483 |
+
<button class="nav-link active" id="navUI" data-bs-toggle="pill" data-bs-target="#demo" type="button" style="color: #1f2937;">Demo UI</button>
|
| 484 |
+
</li>
|
| 485 |
+
<li class="nav-item" id="navRestItem">
|
| 486 |
+
<button class="nav-link" id="navRest" data-bs-toggle="pill" data-bs-target="#rest" type="button" style="color: #1f2937;">Guide</button>
|
| 487 |
+
</li>
|
| 488 |
+
<li class="nav-item" id="navOpenApiItem">
|
| 489 |
+
<button class="nav-link" id="navOpenApi" data-bs-toggle="pill" data-bs-target="#openapi" type="button" style="color: #1f2937;">REST API</button>
|
| 490 |
+
</li>
|
| 491 |
+
</ul>
|
| 492 |
+
</div>
|
| 493 |
+
<div class="ms-auto">
|
| 494 |
+
<div class="dropdown">
|
| 495 |
+
<button class="btn dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" style="background-color: #10b981; color: #ffffff; border-color: #10b981;">
|
| 496 |
+
Data
|
| 497 |
+
</button>
|
| 498 |
+
<div id="testDataButton" class="dropdown-menu" aria-labelledby="dropdownMenuButton"></div>
|
| 499 |
+
</div>
|
| 500 |
+
</div>
|
| 501 |
+
</nav>
|
| 502 |
+
</div>`));
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
const solverforgeFooter = $("footer#solverforge-auto-footer");
|
| 506 |
+
if (solverforgeFooter != null) {
|
| 507 |
+
solverforgeFooter.append(
|
| 508 |
+
$(`<footer class="bg-black text-white-50">
|
| 509 |
+
<div class="container">
|
| 510 |
+
<div class="hstack gap-3 p-4">
|
| 511 |
+
<div class="ms-auto"><a class="text-white" href="https://www.solverforge.org">SolverForge</a></div>
|
| 512 |
+
<div class="vr"></div>
|
| 513 |
+
<div><a class="text-white" href="https://www.solverforge.org/docs">Documentation</a></div>
|
| 514 |
+
<div class="vr"></div>
|
| 515 |
+
<div><a class="text-white" href="https://github.com/SolverForge/solverforge-legacy">Code</a></div>
|
| 516 |
+
<div class="vr"></div>
|
| 517 |
+
<div class="me-auto"><a class="text-white" href="mailto:info@solverforge.org">Support</a></div>
|
| 518 |
+
</div>
|
| 519 |
+
</div>
|
| 520 |
+
</footer>`));
|
| 521 |
+
}
|
| 522 |
+
}
|
static/index.html
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<html lang="en">
|
| 2 |
+
<head>
|
| 3 |
+
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
|
| 4 |
+
<meta content="width=device-width, initial-scale=1" name="viewport">
|
| 5 |
+
<title>Employee scheduling - SolverForge for Python</title>
|
| 6 |
+
|
| 7 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/styles/vis-timeline-graph2d.min.css"
|
| 8 |
+
integrity="sha256-svzNasPg1yR5gvEaRei2jg+n4Pc3sVyMUWeS6xRAh6U=" crossorigin="anonymous">
|
| 9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"/>
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"/>
|
| 11 |
+
<link rel="stylesheet" href="/webjars/solverforge/css/solverforge-webui.css"/>
|
| 12 |
+
<style>
|
| 13 |
+
.vis-time-axis .vis-grid.vis-saturday,
|
| 14 |
+
.vis-time-axis .vis-grid.vis-sunday {
|
| 15 |
+
background: #D3D7CFFF;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
/* Solving spinner */
|
| 19 |
+
#solvingSpinner {
|
| 20 |
+
display: none;
|
| 21 |
+
width: 1.25rem;
|
| 22 |
+
height: 1.25rem;
|
| 23 |
+
border: 2px solid #10b981;
|
| 24 |
+
border-top-color: transparent;
|
| 25 |
+
border-radius: 50%;
|
| 26 |
+
animation: spin 0.75s linear infinite;
|
| 27 |
+
vertical-align: middle;
|
| 28 |
+
}
|
| 29 |
+
#solvingSpinner.active {
|
| 30 |
+
display: inline-block;
|
| 31 |
+
}
|
| 32 |
+
@keyframes spin {
|
| 33 |
+
to { transform: rotate(360deg); }
|
| 34 |
+
}
|
| 35 |
+
</style>
|
| 36 |
+
<link rel="icon" href="/webjars/solverforge/img/solverforge-favicon.svg" type="image/svg+xml">
|
| 37 |
+
</head>
|
| 38 |
+
|
| 39 |
+
<body>
|
| 40 |
+
<header id="solverforge-auto-header">
|
| 41 |
+
<!-- Filled in by app.js -->
|
| 42 |
+
</header>
|
| 43 |
+
<div class="tab-content">
|
| 44 |
+
<div id="demo" class="tab-pane fade show active container-fluid">
|
| 45 |
+
<div class="sticky-top d-flex justify-content-center align-items-center" aria-live="polite"
|
| 46 |
+
aria-atomic="true">
|
| 47 |
+
<div id="notificationPanel" style="position: absolute; top: .5rem;"></div>
|
| 48 |
+
</div>
|
| 49 |
+
<h1>Employee scheduling solver</h1>
|
| 50 |
+
<p>Generate the optimal schedule for your employees.</p>
|
| 51 |
+
|
| 52 |
+
<div class="mb-4">
|
| 53 |
+
<button id="solveButton" type="button" class="btn btn-success">
|
| 54 |
+
<span class="fas fa-play"></span> Solve
|
| 55 |
+
</button>
|
| 56 |
+
<button id="stopSolvingButton" type="button" class="btn btn-danger">
|
| 57 |
+
<span class="fas fa-stop"></span> Stop solving
|
| 58 |
+
</button>
|
| 59 |
+
<span id="solvingSpinner" class="ms-2"></span>
|
| 60 |
+
<span id="unassignedShifts" class="ms-2 align-middle fw-bold"></span>
|
| 61 |
+
<span id="score" class="score ms-2 align-middle fw-bold">Score: ?</span>
|
| 62 |
+
<button id="analyzeButton" type="button" class="ms-2 btn btn-secondary">
|
| 63 |
+
<span class="fas fa-question"></span>
|
| 64 |
+
</button>
|
| 65 |
+
|
| 66 |
+
<div class="float-end">
|
| 67 |
+
<ul class="nav nav-pills" role="tablist">
|
| 68 |
+
<li class="nav-item" role="presentation">
|
| 69 |
+
<button class="nav-link active" id="byLocationTab" data-bs-toggle="tab"
|
| 70 |
+
data-bs-target="#byLocationPanel" type="button" role="tab"
|
| 71 |
+
aria-controls="byLocationPanel" aria-selected="true">By location
|
| 72 |
+
</button>
|
| 73 |
+
</li>
|
| 74 |
+
<li class="nav-item" role="presentation">
|
| 75 |
+
<button class="nav-link" id="byEmployeeTab" data-bs-toggle="tab"
|
| 76 |
+
data-bs-target="#byEmployeePanel" type="button" role="tab"
|
| 77 |
+
aria-controls="byEmployeePanel" aria-selected="false">By employee
|
| 78 |
+
</button>
|
| 79 |
+
</li>
|
| 80 |
+
</ul>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
<div class="mb-4 tab-content">
|
| 84 |
+
<div class="tab-pane fade show active" id="byLocationPanel" role="tabpanel"
|
| 85 |
+
aria-labelledby="byLocationTab">
|
| 86 |
+
<div id="locationVisualization"></div>
|
| 87 |
+
</div>
|
| 88 |
+
<div class="tab-pane fade" id="byEmployeePanel" role="tabpanel" aria-labelledby="byEmployeeTab">
|
| 89 |
+
<div id="employeeVisualization"></div>
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div id="rest" class="tab-pane fade container-fluid">
|
| 95 |
+
<h1>REST API Guide</h1>
|
| 96 |
+
|
| 97 |
+
<h2>Employee Scheduling solver integration via cURL</h2>
|
| 98 |
+
|
| 99 |
+
<h3>1. Download demo data</h3>
|
| 100 |
+
<pre>
|
| 101 |
+
<button class="btn btn-outline-dark btn-sm float-end"
|
| 102 |
+
onclick="copyTextToClipboard('curl1')">Copy</button>
|
| 103 |
+
<code id="curl1">curl -X GET -H 'Accept:application/json' http://localhost:7860/demo-data/SMALL -o sample.json</code>
|
| 104 |
+
</pre>
|
| 105 |
+
|
| 106 |
+
<h3>2. Post the sample data for solving</h3>
|
| 107 |
+
<p>The POST operation returns a <code>jobId</code> that should be used in subsequent commands.</p>
|
| 108 |
+
<pre>
|
| 109 |
+
<button class="btn btn-outline-dark btn-sm float-end"
|
| 110 |
+
onclick="copyTextToClipboard('curl2')">Copy</button>
|
| 111 |
+
<code id="curl2">curl -X POST -H 'Content-Type:application/json' http://localhost:7860/schedules -d@sample.json</code>
|
| 112 |
+
</pre>
|
| 113 |
+
|
| 114 |
+
<h3>3. Get the current status and score</h3>
|
| 115 |
+
<pre>
|
| 116 |
+
<button class="btn btn-outline-dark btn-sm float-end"
|
| 117 |
+
onclick="copyTextToClipboard('curl3')">Copy</button>
|
| 118 |
+
<code id="curl3">curl -X GET -H 'Accept:application/json' http://localhost:7860/schedules/{jobId}/status</code>
|
| 119 |
+
</pre>
|
| 120 |
+
|
| 121 |
+
<h3>4. Get the complete solution</h3>
|
| 122 |
+
<pre>
|
| 123 |
+
<button class="btn btn-outline-dark btn-sm float-end"
|
| 124 |
+
onclick="copyTextToClipboard('curl4')">Copy</button>
|
| 125 |
+
<code id="curl4">curl -X GET -H 'Accept:application/json' http://localhost:7860/schedules/{jobId}</code>
|
| 126 |
+
</pre>
|
| 127 |
+
|
| 128 |
+
<h3>5. Terminate solving early</h3>
|
| 129 |
+
<pre>
|
| 130 |
+
<button class="btn btn-outline-dark btn-sm float-end"
|
| 131 |
+
onclick="copyTextToClipboard('curl5')">Copy</button>
|
| 132 |
+
<code id="curl5">curl -X DELETE -H 'Accept:application/json' http://localhost:7860/schedules/{id}</code>
|
| 133 |
+
</pre>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<div id="openapi" class="tab-pane fade container-fluid">
|
| 137 |
+
<h1>REST API Reference</h1>
|
| 138 |
+
<div class="ratio ratio-1x1">
|
| 139 |
+
<!-- "scrolling" attribute is obsolete, but e.g. Chrome does not support "overflow:hidden" -->
|
| 140 |
+
<iframe src="/q/swagger-ui" style="overflow:hidden;" scrolling="no"></iframe>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<div class="modal fadebd-example-modal-lg" id="scoreAnalysisModal" tabindex="-1" aria-labelledby="scoreAnalysisModalLabel" aria-hidden="true">
|
| 146 |
+
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
| 147 |
+
<div class="modal-content">
|
| 148 |
+
<div class="modal-header">
|
| 149 |
+
<h1 class="modal-title fs-5" id="scoreAnalysisModalLabel">Score analysis <span id="scoreAnalysisScoreLabel"></span></h1>
|
| 150 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 151 |
+
</div>
|
| 152 |
+
<div class="modal-body" id="scoreAnalysisModalContent">
|
| 153 |
+
<!-- Filled in by app.js -->
|
| 154 |
+
</div>
|
| 155 |
+
<div class="modal-footer">
|
| 156 |
+
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
<footer id="solverforge-auto-footer"></footer>
|
| 163 |
+
|
| 164 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js"></script>
|
| 165 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js"></script>
|
| 166 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
| 167 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-joda/1.11.0/js-joda.min.js"></script>
|
| 168 |
+
<script src="/webjars/solverforge/js/solverforge-webui.js"></script>
|
| 169 |
+
<script src="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/standalone/umd/vis-timeline-graph2d.min.js"
|
| 170 |
+
integrity="sha256-Jy2+UO7rZ2Dgik50z3XrrNpnc5+2PAx9MhL2CicodME=" crossorigin="anonymous"></script>
|
| 171 |
+
<script src="/app.js"></script>
|
| 172 |
+
</body>
|
| 173 |
+
</html>
|
static/webjars/solverforge/css/solverforge-webui.css
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
/* Keep in sync with .navbar height on a large screen. */
|
| 3 |
+
--ts-navbar-height: 109px;
|
| 4 |
+
|
| 5 |
+
--ts-green-1-rgb: #10b981;
|
| 6 |
+
--ts-green-2-rgb: #059669;
|
| 7 |
+
--ts-violet-1-rgb: #3E00FF;
|
| 8 |
+
--ts-violet-2-rgb: #3423A6;
|
| 9 |
+
--ts-violet-3-rgb: #2E1760;
|
| 10 |
+
--ts-violet-4-rgb: #200F4F;
|
| 11 |
+
--ts-violet-5-rgb: #000000; /* TODO FIXME */
|
| 12 |
+
--ts-violet-dark-1-rgb: #b6adfd;
|
| 13 |
+
--ts-violet-dark-2-rgb: #c1bbfd;
|
| 14 |
+
--ts-gray-rgb: #666666;
|
| 15 |
+
--ts-white-rgb: #FFFFFF;
|
| 16 |
+
--ts-light-rgb: #F2F2F2;
|
| 17 |
+
--ts-gray-border: #c5c5c5;
|
| 18 |
+
|
| 19 |
+
--tf-light-rgb-transparent: rgb(242,242,242,0.5); /* #F2F2F2 = rgb(242,242,242) */
|
| 20 |
+
--bs-body-bg: var(--ts-light-rgb); /* link to html bg */
|
| 21 |
+
--bs-link-color: var(--ts-violet-1-rgb);
|
| 22 |
+
--bs-link-hover-color: var(--ts-violet-2-rgb);
|
| 23 |
+
|
| 24 |
+
--bs-navbar-color: var(--ts-white-rgb);
|
| 25 |
+
--bs-navbar-hover-color: var(--ts-white-rgb);
|
| 26 |
+
--bs-nav-link-font-size: 18px;
|
| 27 |
+
--bs-nav-link-font-weight: 400;
|
| 28 |
+
--bs-nav-link-color: var(--ts-white-rgb);
|
| 29 |
+
--ts-nav-link-hover-border-color: var(--ts-violet-1-rgb);
|
| 30 |
+
}
|
| 31 |
+
.btn {
|
| 32 |
+
--bs-btn-border-radius: 1.5rem;
|
| 33 |
+
}
|
| 34 |
+
.btn-primary {
|
| 35 |
+
--bs-btn-bg: var(--ts-violet-1-rgb);
|
| 36 |
+
--bs-btn-border-color: var(--ts-violet-1-rgb);
|
| 37 |
+
--bs-btn-hover-bg: var(--ts-violet-2-rgb);
|
| 38 |
+
--bs-btn-hover-border-color: var(--ts-violet-2-rgb);
|
| 39 |
+
--bs-btn-active-bg: var(--ts-violet-2-rgb);
|
| 40 |
+
--bs-btn-active-border-bg: var(--ts-violet-2-rgb);
|
| 41 |
+
--bs-btn-disabled-bg: var(--ts-violet-1-rgb);
|
| 42 |
+
--bs-btn-disabled-border-color: var(--ts-violet-1-rgb);
|
| 43 |
+
}
|
| 44 |
+
.btn-outline-primary {
|
| 45 |
+
--bs-btn-color: var(--ts-violet-1-rgb);
|
| 46 |
+
--bs-btn-border-color: var(--ts-violet-1-rgb);
|
| 47 |
+
--bs-btn-hover-bg: var(--ts-violet-1-rgb);
|
| 48 |
+
--bs-btn-hover-border-color: var(--ts-violet-1-rgb);
|
| 49 |
+
--bs-btn-active-bg: var(--ts-violet-1-rgb);
|
| 50 |
+
--bs-btn-active-border-color: var(--ts-violet-1-rgb);
|
| 51 |
+
--bs-btn-disabled-color: var(--ts-violet-1-rgb);
|
| 52 |
+
--bs-btn-disabled-border-color: var(--ts-violet-1-rgb);
|
| 53 |
+
}
|
| 54 |
+
.navbar-dark {
|
| 55 |
+
--bs-link-color: var(--ts-violet-dark-1-rgb);
|
| 56 |
+
--bs-link-hover-color: var(--ts-violet-dark-2-rgb);
|
| 57 |
+
--bs-navbar-color: var(--ts-white-rgb);
|
| 58 |
+
--bs-navbar-hover-color: var(--ts-white-rgb);
|
| 59 |
+
}
|
| 60 |
+
.nav-pills {
|
| 61 |
+
--bs-nav-pills-link-active-bg: var(--ts-green-1-rgb);
|
| 62 |
+
}
|
| 63 |
+
.nav-pills .nav-link:hover {
|
| 64 |
+
color: var(--ts-green-1-rgb);
|
| 65 |
+
}
|
| 66 |
+
.nav-pills .nav-link.active:hover {
|
| 67 |
+
color: var(--ts-white-rgb);
|
| 68 |
+
}
|
static/webjars/solverforge/img/solverforge-favicon.svg
ADDED
|
|
static/webjars/solverforge/img/solverforge-horizontal-white.svg
ADDED
|
|
static/webjars/solverforge/img/solverforge-horizontal.svg
ADDED
|
|
static/webjars/solverforge/img/solverforge-logo-stacked.svg
ADDED
|
|
static/webjars/solverforge/js/solverforge-webui.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function replaceSolverForgeAutoHeaderFooter() {
|
| 2 |
+
const solverforgeHeader = $("header#solverforge-auto-header");
|
| 3 |
+
if (solverforgeHeader != null) {
|
| 4 |
+
solverforgeHeader.addClass("bg-black")
|
| 5 |
+
solverforgeHeader.append(
|
| 6 |
+
$(`<div class="container-fluid">
|
| 7 |
+
<nav class="navbar sticky-top navbar-expand-lg navbar-dark shadow mb-3">
|
| 8 |
+
<a class="navbar-brand" href="https://www.solverforge.org">
|
| 9 |
+
<img src="/solverforge/img/solverforge-horizontal-white.svg" alt="SolverForge logo" width="200">
|
| 10 |
+
</a>
|
| 11 |
+
</nav>
|
| 12 |
+
</div>`));
|
| 13 |
+
}
|
| 14 |
+
const solverforgeFooter = $("footer#solverforge-auto-footer");
|
| 15 |
+
if (solverforgeFooter != null) {
|
| 16 |
+
solverforgeFooter.append(
|
| 17 |
+
$(`<footer class="bg-black text-white-50">
|
| 18 |
+
<div class="container">
|
| 19 |
+
<div class="hstack gap-3 p-4">
|
| 20 |
+
<div class="ms-auto"><a class="text-white" href="https://www.solverforge.org">SolverForge</a></div>
|
| 21 |
+
<div class="vr"></div>
|
| 22 |
+
<div><a class="text-white" href="https://www.solverforge.org/docs">Documentation</a></div>
|
| 23 |
+
<div class="vr"></div>
|
| 24 |
+
<div><a class="text-white" href="https://github.com/SolverForge/solverforge-legacy">Code</a></div>
|
| 25 |
+
<div class="vr"></div>
|
| 26 |
+
<div class="me-auto"><a class="text-white" href="mailto:info@solverforge.org">Support</a></div>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
<div id="applicationInfo" class="container text-center"></div>
|
| 30 |
+
</footer>`));
|
| 31 |
+
|
| 32 |
+
applicationInfo();
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function showSimpleError(title) {
|
| 38 |
+
const notification = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="min-width: 50rem"/>`)
|
| 39 |
+
.append($(`<div class="toast-header bg-danger">
|
| 40 |
+
<strong class="me-auto text-dark">Error</strong>
|
| 41 |
+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
| 42 |
+
</div>`))
|
| 43 |
+
.append($(`<div class="toast-body"/>`)
|
| 44 |
+
.append($(`<p/>`).text(title))
|
| 45 |
+
);
|
| 46 |
+
$("#notificationPanel").append(notification);
|
| 47 |
+
notification.toast({delay: 30000});
|
| 48 |
+
notification.toast('show');
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
function showError(title, xhr) {
|
| 52 |
+
var serverErrorMessage = !xhr.responseJSON ? `${xhr.status}: ${xhr.statusText}` : xhr.responseJSON.message;
|
| 53 |
+
var serverErrorCode = !xhr.responseJSON ? `unknown` : xhr.responseJSON.code;
|
| 54 |
+
var serverErrorId = !xhr.responseJSON ? `----` : xhr.responseJSON.id;
|
| 55 |
+
var serverErrorDetails = !xhr.responseJSON ? `no details provided` : xhr.responseJSON.details;
|
| 56 |
+
|
| 57 |
+
if (xhr.responseJSON && !serverErrorMessage) {
|
| 58 |
+
serverErrorMessage = JSON.stringify(xhr.responseJSON);
|
| 59 |
+
serverErrorCode = xhr.statusText + '(' + xhr.status + ')';
|
| 60 |
+
serverErrorId = `----`;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
console.error(title + "\n" + serverErrorMessage + " : " + serverErrorDetails);
|
| 64 |
+
const notification = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="min-width: 50rem"/>`)
|
| 65 |
+
.append($(`<div class="toast-header bg-danger">
|
| 66 |
+
<strong class="me-auto text-dark">Error</strong>
|
| 67 |
+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
| 68 |
+
</div>`))
|
| 69 |
+
.append($(`<div class="toast-body"/>`)
|
| 70 |
+
.append($(`<p/>`).text(title))
|
| 71 |
+
.append($(`<pre/>`)
|
| 72 |
+
.append($(`<code/>`).text(serverErrorMessage + "\n\nCode: " + serverErrorCode + "\nError id: " + serverErrorId))
|
| 73 |
+
)
|
| 74 |
+
);
|
| 75 |
+
$("#notificationPanel").append(notification);
|
| 76 |
+
notification.toast({delay: 30000});
|
| 77 |
+
notification.toast('show');
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// ****************************************************************************
|
| 81 |
+
// Application info
|
| 82 |
+
// ****************************************************************************
|
| 83 |
+
|
| 84 |
+
function applicationInfo() {
|
| 85 |
+
$.getJSON("info", function (info) {
|
| 86 |
+
$("#applicationInfo").append("<small>" + info.application + " (version: " + info.version + ", built at: " + info.built + ")</small>");
|
| 87 |
+
}).fail(function (xhr, ajaxOptions, thrownError) {
|
| 88 |
+
console.warn("Unable to collect application information");
|
| 89 |
+
});
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// ****************************************************************************
|
| 93 |
+
// TangoColorFactory
|
| 94 |
+
// ****************************************************************************
|
| 95 |
+
|
| 96 |
+
const SEQUENCE_1 = [0x8AE234, 0xFCE94F, 0x729FCF, 0xE9B96E, 0xAD7FA8];
|
| 97 |
+
const SEQUENCE_2 = [0x73D216, 0xEDD400, 0x3465A4, 0xC17D11, 0x75507B];
|
| 98 |
+
|
| 99 |
+
var colorMap = new Map;
|
| 100 |
+
var nextColorCount = 0;
|
| 101 |
+
|
| 102 |
+
function pickColor(object) {
|
| 103 |
+
let color = colorMap[object];
|
| 104 |
+
if (color !== undefined) {
|
| 105 |
+
return color;
|
| 106 |
+
}
|
| 107 |
+
color = nextColor();
|
| 108 |
+
colorMap[object] = color;
|
| 109 |
+
return color;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
function nextColor() {
|
| 113 |
+
let color;
|
| 114 |
+
let colorIndex = nextColorCount % SEQUENCE_1.length;
|
| 115 |
+
let shadeIndex = Math.floor(nextColorCount / SEQUENCE_1.length);
|
| 116 |
+
if (shadeIndex === 0) {
|
| 117 |
+
color = SEQUENCE_1[colorIndex];
|
| 118 |
+
} else if (shadeIndex === 1) {
|
| 119 |
+
color = SEQUENCE_2[colorIndex];
|
| 120 |
+
} else {
|
| 121 |
+
shadeIndex -= 3;
|
| 122 |
+
let floorColor = SEQUENCE_2[colorIndex];
|
| 123 |
+
let ceilColor = SEQUENCE_1[colorIndex];
|
| 124 |
+
let base = Math.floor((shadeIndex / 2) + 1);
|
| 125 |
+
let divisor = 2;
|
| 126 |
+
while (base >= divisor) {
|
| 127 |
+
divisor *= 2;
|
| 128 |
+
}
|
| 129 |
+
base = (base * 2) - divisor + 1;
|
| 130 |
+
let shadePercentage = base / divisor;
|
| 131 |
+
color = buildPercentageColor(floorColor, ceilColor, shadePercentage);
|
| 132 |
+
}
|
| 133 |
+
nextColorCount++;
|
| 134 |
+
return "#" + color.toString(16);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
function buildPercentageColor(floorColor, ceilColor, shadePercentage) {
|
| 138 |
+
let red = (floorColor & 0xFF0000) + Math.floor(shadePercentage * ((ceilColor & 0xFF0000) - (floorColor & 0xFF0000))) & 0xFF0000;
|
| 139 |
+
let green = (floorColor & 0x00FF00) + Math.floor(shadePercentage * ((ceilColor & 0x00FF00) - (floorColor & 0x00FF00))) & 0x00FF00;
|
| 140 |
+
let blue = (floorColor & 0x0000FF) + Math.floor(shadePercentage * ((ceilColor & 0x0000FF) - (floorColor & 0x0000FF))) & 0x0000FF;
|
| 141 |
+
return red | green | blue;
|
| 142 |
+
}
|