diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..668f73a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [blackopsrepl] diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..18e35e0 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,172 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - main + - dev + - 'release/**' + paths: + - 'rust/employee-scheduling/**' + - 'legacy/employee-scheduling-fast/**' + - 'legacy/maintenance-scheduling-fast/**' + - 'legacy/meeting-scheduling-fast/**' + - 'legacy/order-picking-fast/**' + - 'legacy/vehicle-routing-fast/**' + - 'legacy/vm-placement-fast/**' + - '.github/workflows/docker-publish.yml' + workflow_dispatch: + inputs: + apps: + description: 'Apps to build (comma-separated, or "all")' + required: false + default: 'all' + +env: + REGISTRY: ghcr.io + +jobs: + # Rust tests + test-rust: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: rust/employee-scheduling + + - name: Run tests + working-directory: rust/employee-scheduling + run: cargo test --verbose + + - name: Build + working-directory: rust/employee-scheduling + run: cargo build --release --verbose + + # Python legacy tests + test-python: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + app: + - name: employee-scheduling-fast + path: legacy/employee-scheduling-fast + - name: maintenance-scheduling-fast + path: legacy/maintenance-scheduling-fast + - name: meeting-scheduling-fast + path: legacy/meeting-scheduling-fast + - name: order-picking-fast + path: legacy/order-picking-fast + - name: vehicle-routing-fast + path: legacy/vehicle-routing-fast + - name: vm-placement-fast + path: legacy/vm-placement-fast + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Install dependencies + working-directory: ${{ matrix.app.path }} + run: | + python -m pip install --upgrade pip + pip install -e . + pip install pytest + + - name: Run tests + working-directory: ${{ matrix.app.path }} + run: pytest -v + + # Build and push on main and dev + build-and-push: + needs: [test-rust, test-python] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + app: + - name: employee-scheduling + context: rust/employee-scheduling + - name: employee-scheduling-fast + context: legacy/employee-scheduling-fast + - name: maintenance-scheduling-fast + context: legacy/maintenance-scheduling-fast + - name: meeting-scheduling-fast + context: legacy/meeting-scheduling-fast + - name: order-picking-fast + context: legacy/order-picking-fast + - name: vehicle-routing-fast + context: legacy/vehicle-routing-fast + - name: vm-placement-fast + context: legacy/vm-placement-fast + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check if app should be built + id: check + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + apps="${{ github.event.inputs.apps }}" + if [[ "$apps" == "all" || "$apps" == *"${{ matrix.app.name }}"* ]]; then + echo "build=true" >> $GITHUB_OUTPUT + else + echo "build=false" >> $GITHUB_OUTPUT + fi + else + echo "build=true" >> $GITHUB_OUTPUT + fi + + - name: Set up Docker Buildx + if: steps.check.outputs.build == 'true' + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: steps.check.outputs.build == 'true' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + if: steps.check.outputs.build == 'true' + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.app.name }} + tags: | + type=ref,event=branch + type=sha,prefix= + type=raw,value=latest + + - name: Build and push Docker image + if: steps.check.outputs.build == 'true' + uses: docker/build-push-action@v5 + with: + context: ${{ matrix.app.context }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index a4d221d..b5484cc 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ __pycache__ # Repository wide ignore mac DS_Store files .DS_Store +.osm_cache *.code-workspace +CLAUDE.md diff --git a/README.md b/README.md index 462e283..9689a68 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,35 @@ -

- - Timefold Solver - -

+# SolverForge Quickstarts -This repository contains unofficial Python quickstarts for [Timefold Solver](https://github.com/TimefoldAI/timefold-solver-python), an AI constraint solver. The Python solver has been [discontinued by Timefold](https://github.com/TimefoldAI/timefold-solver/discussions/1698#discussioncomment-13842196) and the original team is focusing on the Java and Kotlin solvers. -A roadmap to provide community support for Python is currently under review. - -Priority for this repository is closing the performance gap between the Java and Python quickstarts by providing optimized solutions for all use cases. - -- The original quickstarts have been moved to [legacy/](legacy/). These are based on unified Pydantic data models for API and constraint solving -- In [fast/](fast/) we are incrementally refactoring quickstarts to employ more efficient data models that limit Pydantic to API boundary validation to reduce overhead during solver moves -- In [benchmarks/](benchmarks/) we are running benchmarks to assess the performance of the different implementations - -Current results are available in the [benchmarks](benchmarks/) directory: -- [Meeting Scheduling Benchmark](benchmarks/results_meeting-scheduling.md) -- [Vehicle Routing Benchmark](benchmarks/results_vehicle-routing.md) - -You can also find our assessment of said results in [report.md](benchmarks/report.md). - -Currently, *fast* quickstarts are available for the [employee-scheduling](fast/employee-scheduling-fast/README.MD), [meeting scheduling](fast/meeting-scheduling-fast/README.MD) and [vehicle-routing](fast/vehicle-routing-fast/README.MD) use cases, exclusively. - -Being unofficial, this repository is not directly affiliated with Timefold AI, but maintainers are in touch with the Timefold AI team. - -It shows different use cases and basic implementations of constraint solving in Python. +This repository contains quickstarts for [SolverForge](https://github.com/SolverForge/solverforge), an AI constraint solver and framework for Rust and Python. +It shows different use cases and basic implementations of constraint solving. ## Overview -| Use Case | Notable Solver Concepts | -|-----------------------------------------------------------------------|----------------------------------------------------------| -| ๐Ÿšš Vehicle Routing | Chained Through Time, Shadow Variables | -| ๐Ÿง‘โ€๐Ÿ’ผ Employee Scheduling | Load Balancing | -| ๐Ÿ› ๏ธ Maintenance Scheduling | TimeGrain, Shadow Variable, Variable Listener | -| ๐Ÿ“ฆ Food Packaging | Chained Through Time, Shadow Variables, Pinning | -| ๐Ÿ›’ Order Picking | Chained Planning Variable, Shadow Variables | -| ๐Ÿซ School Timetabling | Timeslot | -| ๐Ÿญ Facility Location Problem | Shadow Variable | -| ๐ŸŽค Conference Scheduling | Timeslot, Justifications | -| ๐Ÿ›๏ธ Bed Allocation Scheduling | Allows Unassigned | -| ๐Ÿ›ซ Flight Crew Scheduling | | -| ๐Ÿ‘ฅ Meeting Scheduling | TimeGrain | -| โœ… Task Assigning | Bendable Score, Chained Through Time, Allows Unassigned | -| ๐Ÿ“† Project Job Scheduling | Shadow Variables, Variable Listener, Strenght Comparator | -| ๐Ÿ† Sports League Scheduling | Consecutive Sequences | -| ๐Ÿ… Tournament Scheduling | Pinning, Load Balancing | +| Use Case | Rust | Python (Legacy) | Notable Solver Concepts | +|-----------------------------------------------------------------------|------|-----------------|-----------------------------------------------------| +| ๐Ÿ‘‹ Hello World | ๐Ÿšง | โœ… | Basic Planning Problem | +| ๐Ÿง‘โ€๐Ÿ’ผ Employee Scheduling | โœ… | โœ… | Load Balancing | +| ๐Ÿšš Vehicle Routing | ๐Ÿšง | โœ… | Chained Through Time, Shadow Variables | +| ๐Ÿ› ๏ธ Maintenance Scheduling | ๐Ÿšง | โœ… | TimeGrain, Shadow Variable, Variable Listener | +| ๐Ÿ›’ Order Picking | ๐Ÿšง | โœ… | Chained Planning Variable, Shadow Variables | +| ๐Ÿ‘ฅ Meeting Scheduling | ๐Ÿšง | โœ… | TimeGrain | +| ๐Ÿ“ˆ Portfolio Optimization | ๐Ÿšง | โœ… | Financial Constraints | +| ๐Ÿ–ฅ๏ธ VM Placement | ๐Ÿšง | โœ… | Bin Packing, Resource Allocation | > [!NOTE] > The implementations in this repository serve as a starting point and/or inspiration when creating your own application. -> Timefold Solver is a library and does not include a UI. To illustrate these use cases a rudimentary UI is included in these quickstarts. +> SolverForge is a library and does not include a UI. To illustrate these use cases a rudimentary UI is included in these quickstarts. +> +> **Rust implementations** are native SolverForge applications showcasing zero-erasure architecture. +> **Python (Legacy)** implementations use the Timefold-based legacy solver and are located in the [legacy/](legacy/) directory. ## Use cases -### ๐Ÿšš Vehicle Routing - -Find the most efficient routes for vehicles to reach visits, considering vehicle capacity and time windows when visits are available. Sometimes also called "CVRPTW". - -![Vehicle Routing Screenshot](legacy/vehicle-routing/vehicle-routing-screenshot.png) +### ๐Ÿ‘‹ Hello World -- [Run vehicle-routing](legacy/vehicle-routing/README.MD) (Python, FastAPI) -- [Run vehicle-routing (fast)](fast/vehicle-routing-fast/README.MD) (Python, FastAPI) +A minimal example demonstrating the basics of constraint solving with SolverForge. -> [!TIP] -> [Check out our off-the-shelf model for Field Service Routing](https://app.timefold.ai/models/field-service-routing). This model goes beyond basic Vehicle Routing and supports additional constraints such as priorities, skills, fairness and more. +- **Python (Legacy)**: [legacy/hello-world-fast](legacy/hello-world-fast/README.md) --- @@ -69,33 +37,24 @@ Find the most efficient routes for vehicles to reach visits, considering vehicle Schedule shifts to employees, accounting for employee availability and shift skill requirements. -![Employee Scheduling Screenshot](java/employee-scheduling/employee-scheduling-screenshot.png) - -- [Run employee-scheduling](legacy/employee-scheduling/README.MD) (Python, FastAPI) -- [Run employee-scheduling (fast)](fast/employee-scheduling-fast/README.MD) (Python, FastAPI) - -> [!TIP] -> [Check out our off-the-shelf model for Employee Shift Scheduling](https://app.timefold.ai/models/employee-scheduling). This model supports many additional constraints such as skills, pairing employees, fairness and more. +- **Rust**: [rust/employee-scheduling](rust/employee-scheduling/README.md) +- **Python (Legacy)**: [legacy/employee-scheduling-fast](legacy/employee-scheduling-fast/README.md) --- -### ๐Ÿ› ๏ธ Maintenance Scheduling - -Schedule maintenance jobs to crews over time to reduce both premature and overdue maintenance. +### ๐Ÿšš Vehicle Routing -![Maintenance Scheduling Screenshot](legacy/maintenance-scheduling/maintenance-scheduling-screenshot.png) +Find the most efficient routes for vehicles to reach visits, considering vehicle capacity and time windows when visits are available. Sometimes also called "CVRPTW". -- [Run maintenance-scheduling](legacy/maintenance-scheduling/README.adoc) (Python, FastAPI) +- **Python (Legacy)**: [legacy/vehicle-routing-fast](legacy/vehicle-routing-fast/README.md) --- -### ๐Ÿ“ฆ Food Packaging - -Schedule food packaging orders to manufacturing lines to minimize downtime and fulfill all orders on time. +### ๐Ÿ› ๏ธ Maintenance Scheduling -![Food Packaging Screenshot](legacy/food-packaging/food-packaging-screenshot.png) +Schedule maintenance jobs to crews over time to reduce both premature and overdue maintenance. -- [Run food-packaging](legacy/food-packaging/README.adoc) (Python, FastAPI) +- **Python (Legacy)**: [legacy/maintenance-scheduling-fast](legacy/maintenance-scheduling-fast/README.md) --- @@ -103,63 +62,7 @@ Schedule food packaging orders to manufacturing lines to minimize downtime and f Generate an optimal picking plan for completing a set of orders. -![Order Picking Screenshot](legacy/order-picking/order-picking-screenshot.png) - -- [Run order-picking](legacy/order-picking/README.adoc) (Python, FastAPI) - ---- - -### ๐Ÿซ School Timetabling - -Assign lessons to timeslots and rooms to produce a better schedule for teachers and students. - -![School Timetabling Screenshot](legacy/school-timetabling/school-timetabling-screenshot.png) - -- [Run school-timetabling](legacy/school-timetabling/README.adoc) (Python, FastAPI) - -Without a UI: - -- [Run hello-world-school-timetabling](legacy/hello-world/README.adoc) (Java, Maven or Gradle) - ---- - -### ๐Ÿญ Facility Location Problem - -Pick the best geographical locations for new stores, distribution centers, COVID test centers, or telecom masts. - -![Facility Location Screenshot](legacy/facility-location/facility-location-screenshot.png) - -- [Run facility-location](legacy/facility-location/README.adoc) (Python, FastAPI) - ---- - -### ๐ŸŽค Conference Scheduling - -Assign conference talks to timeslots and rooms to produce a better schedule for speakers. - -![Conference Scheduling Screenshot](legacy/conference-scheduling/conference-scheduling-screenshot.png) - -- [Run conference-scheduling](legacy/conference-scheduling/README.adoc) (Python, FastAPI) - ---- - -### ๐Ÿ›๏ธ Bed Allocation Scheduling - -Assign beds to patient stays to produce a better schedule for hospitals. - -![Bed Scheduling Screenshot](legacy/bed-allocation/bed-scheduling-screenshot.png) - -- [Run bed-allocation-scheduling](legacy/bed-allocation/README.adoc) (Python, FastAPI) - ---- - -### ๐Ÿ›ซ Flight Crew Scheduling - -Assign crew to flights to produce a better schedule for flight assignments. - -![Flight Crew Scheduling Screenshot](legacy/flight-crew-scheduling/flight-crew-scheduling-screenshot.png) - -- [Run flight-crew-scheduling](legacy/flight-crew-scheduling/README.adoc) (Python, FastAPI) +- **Python (Legacy)**: [legacy/order-picking-fast](legacy/order-picking-fast/README.md) --- @@ -167,58 +70,31 @@ Assign crew to flights to produce a better schedule for flight assignments. Assign timeslots and rooms for meetings to produce a better schedule. -![Meeting Scheduling Screenshot](legacy/meeting-scheduling/meeting-scheduling-screenshot.png) - -- [Run meeting-scheduling](legacy/meeting-scheduling/README.adoc) (Python, FastAPI) -- [Run meeting-scheduling (fast)](fast/meeting-scheduling-fast/README.adoc) (Python, FastAPI) +- **Python (Legacy)**: [legacy/meeting-scheduling-fast](legacy/meeting-scheduling-fast/README.md) --- -### โœ… Task Assigning - -Assign employees to tasks to produce a better plan for task assignments. +### ๐Ÿ“ˆ Portfolio Optimization -![Task Assigning Screenshot](legacy/task-assigning/task-assigning-screenshot.png) +Optimize investment portfolios to balance risk and return while satisfying various financial constraints. -- [Run task-assigning](legacy/task-assigning/README.adoc) (Python, FastAPI) +- **Python (Legacy)**: [legacy/portfolio-optimization-fast](legacy/portfolio-optimization-fast/README.md) --- -### ๐Ÿ“† Project Job Scheduling +### ๐Ÿ–ฅ๏ธ VM Placement -Assign jobs for execution to produce a better schedule for project job allocations. +Optimize the placement of virtual machines across physical servers to maximize resource utilization and minimize costs. -![Project Job Scheduling Screenshot](legacy/project-job-scheduling/project-job-scheduling-screenshot.png) - -- [Run project-job-scheduling](legacy/project-job-scheduling/README.adoc) (Python, FastAPI) - ---- - -### ๐Ÿ† Sports League Scheduling - -Assign rounds to matches to produce a better schedule for league matches. - -![Sports League Scheduling Screenshot](legacy/sports-league-scheduling/sports-league-scheduling-screenshot.png) - -- [Run sports-league-scheduling](legacy/sports-league-scheduling/README.adoc) (Python, FastAPI) - ---- - -### ๐Ÿ… Tournament Scheduling - -Tournament Scheduling service assigning teams to tournament matches. - -![Tournament Scheduling Screenshot](legacy/tournament-scheduling/tournament-scheduling-screenshot.png) - -- [Run tournament-scheduling](legacy/tournament-scheduling/README.adoc) (Python, FastAPI) +- **Python (Legacy)**: [legacy/vm-placement-fast](legacy/vm-placement-fast/README.md) --- ## Legal notice -This version of Timefold Quickstarts was forked on 03 August 2025 from the original Timefold Quickstarts, which was entirely Apache-2.0 licensed (a permissive license). +This version of SolverForge is inspired on a repo that was forked on 03 August 2025 from the original Timefold Quickstarts, which was entirely Apache-2.0 licensed (a permissive license). Derivative work is limited to the [legacy/](legacy/) folder and the original fork is available as an archive. The original Timefold Quickstarts was [forked](https://timefold.ai/blog/2023/optaplanner-fork/) on 20 April 2023 from OptaPlanner Quickstarts. -This version of Timefold Quickstarts is a derivative work of the original Timefold Quickstarts and OptaPlanner Quickstarts, which includes copyrights of the original creators, Timefold AI, Red Hat Inc., affiliates, and contributors, that were all entirely licensed under the Apache-2.0 license. -Every source file has been modified. +This version of SolverForge Quickstarts is inspired on the original Timefold Quickstarts and OptaPlanner Quickstarts, which includes copyrights of the original creators, Timefold AI, Red Hat Inc., affiliates, and contributors, that were all entirely licensed under the Apache-2.0 license, for the [legacy/](legacy/) folder exclusively. Every source file has been modified. + diff --git a/benchmarks/README.md b/benchmarks/README.md deleted file mode 100644 index e4f2986..0000000 --- a/benchmarks/README.md +++ /dev/null @@ -1,149 +0,0 @@ -# ๐ŸŽฏ Benchmark Framework Documentation - -## ๐Ÿš€ Setup Instructions - -### 1. Start Backend Servers - -```bash -# Terminal 1 - Original Python Backend -uvicorn src.meeting_scheduling:app --host 127.0.0.1 --port 8081 - -# Terminal 2 - Fast Python Backend -uvicorn src.meeting_scheduling:app --host 127.0.0.1 --port 8082 - -# Terminal 3 - Java Backend -cd quickstarts/java/meeting-scheduling -mvn quarkus:dev - -### 2. Run Benchmark - -```bash -# Run all scenarios -python quickstarts/benchmark/benchmark_meeting_scheduling.py - -# Run specific test -python quickstarts/benchmark/benchmark_meeting_scheduling.py --test "Python Backend (FAST) - Java Demo Data" - -# Run with multiple iterations -python quickstarts/benchmark/benchmark_meeting_scheduling.py --iterations 5 - -# Save results to markdown file -python quickstarts/benchmark/benchmark_meeting_scheduling.py --output-file results.md -``` - -## ๐Ÿ› ๏ธ Technical Implementation - -### Benchmark Architecture - -```python -@dataclass -class BenchmarkResult: - data_source: str - job_id: str - solve_time_ms: int - final_score: Dict[str, int] - solver_iterations: int - success: bool - analysis: Optional[Dict] = None - error_message: Optional[str] = None -``` - -### Data Conversion - -#### Java โ†’ Python Format -- Extracts nested attendances from meetings -- Converts person references to full objects -- Maintains data integrity across platforms - -#### Python โ†’ Java Format -- Groups attendances by meeting ID -- Converts person objects to string IDs -- Adds nested attendance structures - -### Solver Configuration -```python -"solverConfiguration": { - "termination": { - "secondsSpentLimit": 30, - "unimprovedSecondsSpentLimit": 30 - } -} -``` - -## ๐Ÿ“ˆ Performance Analysis - -### Current Limitations -1. **Small Dataset**: Current demo data may be too small to reveal true performance differences -2. **Time Limits**: 30-second termination masks performance characteristics -3. **Memory Constraints**: Not currently measured - -### Key Findings - -**Performance Comparison:** -- **Java vs FAST Python**: Comparable performance (~0.5% difference) -- **Original Python**: Performance already degraded with fewer iterations completed (46-58 vs 60) and constraint evaluation bugs (-85medium violations) - -**Theoretical Scaling Prediction:** -- **Original Python**: Validation overhead increases with dataset size (complexity unknown) -- **FAST Python/Java**: No validation overhead during solving - -**Note:** Actual scaling behavior not yet measured. - -## ๐ŸŽฏ Recommendations - -### 1. **Architectural Recommendations** -- **Python Backend (FAST)** proves to be perfectly viable for production deployment for the Meeting Scheduling Problem with a small dataset -- **Java Backend** remains the reference implementation, with proven performance and correctness -- **Avoid Original Python Backend with Pydantic domain models** due to constraint evaluation bugs and incomplete optimization - -In more detail: - -- **Use dataclass domain models** for constraint solving implementations -- **Reserve Pydantic models** for API serialization/deserialization only -- **Maintain clean separation** between domain logic and validation logic -- **Monitor optimization completion rates** as a key performance indicator - -## ๐Ÿ”ง Troubleshooting - -### Common Issues - -#### Server Connection Errors -```bash -โŒ Python server not running at http://127.0.0.1:8081 - Please start the Python server first with: - uvicorn src.meeting_scheduling:app --host 127.0.0.1 --port 8081 -``` - -#### Data Conversion Errors -- Ensure both servers are running for cross-platform tests -- Check data format compatibility -- Verify constraint definitions match - -#### Timeout Issues -- Increase solver termination limits for larger datasets -- Monitor system resources during solving -- Check for memory leaks in long-running sessions - -## ๐Ÿ“ Output Formats - -### Console Output -- Real-time progress indicators -- Detailed constraint analysis -- Performance comparison summary - -### Markdown Output -- Comprehensive results table -- Performance analytics -- Detailed constraint breakdown -- Professional reporting format - -## ๐Ÿ”„ Future Enhancements - -### Research Areas -1. **Porting the benchmark to other problems** - -### Planned Improvements -1. **Larger dataset generation** -2. **Memory usage tracking** -3. **Performance profiling** -4. **Regression testing** \ No newline at end of file diff --git a/benchmarks/benchmark_meeting_scheduling.py b/benchmarks/benchmark_meeting_scheduling.py deleted file mode 100755 index e4bfc0b..0000000 --- a/benchmarks/benchmark_meeting_scheduling.py +++ /dev/null @@ -1,1130 +0,0 @@ -#!/usr/bin/env python3 - -import json, sys, time, traceback, click, requests - -from dataclasses import dataclass -from typing import Dict, List, Optional -from collections import defaultdict -import statistics - - -@dataclass -class BenchmarkConfig: - """Configuration for the benchmark tool.""" - python_base_url: str = "http://127.0.0.1:8081" - python2_base_url: str = "http://127.0.0.1:8082" - java_base_url: str = "http://127.0.0.1:8080" - - -@dataclass -class BenchmarkResult: - """Result of a single benchmark run.""" - data_source: str - job_id: str - solve_time_ms: int - final_score: Dict[str, int] - solver_iterations: int - success: bool - analysis: Optional[Dict] = None - error_message: Optional[str] = None - - -@click.command() -@click.option('--iterations', default=1, help='Number of iterations per test') -@click.option('--output-file', type=click.Path(), help='Output results to markdown file') -@click.option('--test', help='Run only a specific test by name') -def main(iterations, output_file, test): - """Meeting Scheduler Benchmark Tool""" - config = BenchmarkConfig() - - click.echo("\n๐Ÿ”ง Meeting Scheduler Benchmark Tool") - click.echo("="*50) - - # Check both servers - python_server_ok = check_server(config.python_base_url) - python2_server_ok = check_server(config.python2_base_url) - java_server_ok = check_server(config.java_base_url) - - if not python_server_ok: - click.echo(f"โŒ Python server not running at {config.python_base_url}") - click.echo(" Please start the Python server first with:") - click.echo(f" uvicorn src.meeting_scheduling:app --host {config.python_base_url.split('//')[1].split(':')[0]} --port {config.python_base_url.split(':')[-1]}") - - if not python2_server_ok: - click.echo(f"โŒ Python2 server not running at {config.python2_base_url}") - click.echo(" Please start the Python2 server first with:") - click.echo(f" uvicorn src.meeting_scheduling:app --host {config.python2_base_url.split('//')[1].split(':')[0]} --port {config.python2_base_url.split(':')[-1]}") - - if not java_server_ok: - click.echo(f"โŒ Java server not running at {config.java_base_url}") - click.echo(" Please start the Java server first") - - if not python_server_ok and not python2_server_ok and not java_server_ok: - # Still write to file if requested, even if servers are down - if output_file: - click.echo(f"๐Ÿ“„ Writing error status to {output_file}") - with open(output_file, 'w') as f: - f.write("# Meeting Scheduler Benchmark Results\n\n") - f.write(f"Generated on: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n") - f.write("## โŒ Server Error\n\n") - f.write("No servers running:\n") - f.write(f"- Python server: {config.python_base_url}\n") - f.write(f"- Python2 server: {config.python2_base_url}\n") - f.write(f"- Java server: {config.java_base_url}\n") - - sys.exit(1) - - if python_server_ok: - click.echo(f"โœ… Python server is running at {config.python_base_url}") - - if python2_server_ok: - click.echo(f"โœ… Python2 server is running at {config.python2_base_url}") - - if java_server_ok: - click.echo(f"โœ… Java server is running at {config.java_base_url}") - - click.echo() # Add spacing after server status - - # Define test scenarios - test_scenarios = { - "Python Backend - Python Demo Data": { - "condition": python_server_ok, - "fetcher": lambda: get_demo_data(config.python_base_url), - "converter": None, - "backend": config.python_base_url - }, - "Python Backend - Java Demo Data": { - "condition": python_server_ok and java_server_ok, - "fetcher": lambda: get_demo_data(config.java_base_url), - "converter": lambda data: convert_java_to_python_format(data), - "backend": config.python_base_url - }, - "Python Backend (FAST) - Python Demo Data": { - "condition": python2_server_ok, - "fetcher": lambda: get_demo_data(config.python2_base_url), - "converter": None, - "backend": config.python2_base_url - }, - "Python Backend (FAST) - Java Demo Data": { - "condition": python2_server_ok and java_server_ok, - "fetcher": lambda: get_demo_data(config.java_base_url), - "converter": lambda data: convert_java_to_python_format(data), - "backend": config.python2_base_url - }, - "Java Backend - Java Demo Data": { - "condition": java_server_ok, - "fetcher": lambda: get_demo_data(config.java_base_url), - "converter": None, - "backend": config.java_base_url - }, - "Java Backend - Python Demo Data": { - "condition": java_server_ok and python_server_ok, - "fetcher": lambda: get_demo_data(config.python_base_url), - "converter": lambda data: convert_python_to_java_format(data), - "backend": config.java_base_url - } - } - - all_results = [] - - # Run tests - if test: - if test in test_scenarios: - scenario = test_scenarios[test] - if scenario['condition']: - results = run_test_scenario( - test, - scenario['fetcher'], - scenario['converter'], - scenario['backend'], - iterations - ) - all_results.extend(results) - else: - click.echo(f"โš ๏ธ Skipping test '{test}' - required server not running.") - else: - click.echo(f"โŒ Unknown test: {test}") - click.echo(f" Available tests: {list(test_scenarios.keys())}") - else: - # Run all applicable tests - for name, scenario in test_scenarios.items(): - if scenario['condition']: - results = run_test_scenario( - name, - scenario['fetcher'], - scenario['converter'], - scenario['backend'], - iterations - ) - all_results.extend(results) - - print_results(all_results, output_file) - - -def run_test_scenario(test_name, data_fetcher, data_converter=None, target_backend=None, iterations=1): - """Generic test runner using closures to avoid repetition.""" - click.echo(f"๐Ÿ“ฅ Testing {test_name}...") - click.echo("-" * 60) - - def analyze_solution(data, backend, original_data=None): - """Analyze solution with detailed output following proper REST API flow.""" - click.echo(f" ๐Ÿ” Analyzing final solution...") - - # Step 1: Get the complete solution from the solver (REST API step 4) - solved_data = data - if hasattr(result, 'job_id') and result.job_id: - try: - click.echo(f" ๐Ÿ“ฅ Fetching complete solution for job {result.job_id}...") - response = requests.get(f"{backend}/schedules/{result.job_id}") - - if response.status_code == 200: - solved_data = response.json() - click.echo(f" โœ… Retrieved solved schedule for analysis") - - else: - click.echo(f" โš ๏ธ Could not retrieve solved schedule (status {response.status_code})") - click.echo(f" โš ๏ธ Using input data for analysis") - - except Exception as e: - click.echo(f" โš ๏ธ Error retrieving solved schedule: {e}") - click.echo(f" โš ๏ธ Using input data for analysis") - - # Step 2: Prepare analysis payload (fix data if needed for cross-format scenarios) - analysis_payload = solved_data - if original_data: - analysis_payload = prepare_for_analysis(solved_data, original_data) - - # Step 3: Call /analyze endpoint with the SOLUTION data (REST API step 5) - click.echo(f" ๐Ÿ”ฌ Calling /analyze endpoint with solution data...") - analysis = analyze_schedule(analysis_payload, backend) - - if analysis: - constraint_count = len(analysis.get('constraints', [])) - click.echo(f" ๐Ÿ“Š Analysis complete. Found {constraint_count} constraints.") - return analysis - - else: - click.echo(f" โŒ Analysis failed") - return None - - results = [] - for i in range(iterations): - click.echo(f"\n ๐Ÿ“ฅ Fetching demo data for iteration {i+1}...") - - # Fetch data using the provided closure - demo_data = data_fetcher() - - if demo_data: - click.echo(f" โœ… Got demo data with {len(demo_data.get('meetings', []))} meetings") - - # Keep a copy of the original data before any conversion - original_demo_data = json.loads(json.dumps(demo_data)) - - # Convert data if converter is provided - if data_converter: - demo_data = data_converter(demo_data) - - # Run benchmark - result = run_benchmark(demo_data, f"{test_name} (Run {i+1})", target_backend) - - # Analyze the final solution and store it - if result.success: - # For Java -> Python conversion, we need to use the original data for analysis - if "Java Demo Data" in test_name and "Python Backend" in test_name: - analysis_result = analyze_solution(demo_data, target_backend, original_data=original_demo_data) - - else: - analysis_result = analyze_solution(demo_data, target_backend) - - result.analysis = analysis_result - - results.append(result) - - else: - click.echo(f" โŒ Failed to get demo data for iteration {i+1}") - # Add a failed result - results.append(BenchmarkResult( - data_source=f"{test_name} (Run {i+1})", - job_id="", - solve_time_ms=0, - final_score={}, - solver_iterations=0, - success=False, - error_message="Failed to get demo data" - )) - - click.echo() # Add spacing after test - return results - - -def write_markdown_file(results: List[BenchmarkResult], output_file: str): - """Write results to markdown file with maximum spiff.""" - def create_header(): - """Create spiff markdown header.""" - return [ - "# ๐ŸŽฏ Meeting Scheduler Benchmark Results", - "", - f"๐Ÿ“… **Generated:** {time.strftime('%Y-%m-%d %H:%M:%S')} ", - f"๐Ÿš€ **Total Scenarios:** {len(results)} ", - f"โœ… **Successful Runs:** {len([r for r in results if r.success])} ", - "", - "---", - "" - ] - - def create_results_table(): - """Create spiff results table.""" - lines = [ - "## ๐Ÿ“Š Individual Results", - "", - "| ๐Ÿท๏ธ Scenario | ๐Ÿ“ˆ Status | โšก Time | ๐ŸŽฏ Score | ๐Ÿ”„ Iterations | โŒ Error |", - "|-------------|-----------|---------|----------|---------------|----------|" - ] - - for result in results: - status_icon = "๐ŸŸข" if result.success else "๐Ÿ”ด" - status_text = "Success" if result.success else "Failed" - - if result.success: - # Add performance indicators - time_indicator = "๐Ÿš€" if result.solve_time_ms < 25000 else "โšก" if result.solve_time_ms < 35000 else "๐ŸŒ" - lines.append(f"| **{result.data_source}** | {status_icon} {status_text} | {time_indicator} {result.solve_time_ms:,}ms | `{format_score(result.final_score)}` | {result.solver_iterations} | - |") - - else: - lines.append(f"| **{result.data_source}** | {status_icon} {status_text} | - | - | - | {result.error_message} |") - - return lines - - def create_summary(): - """Create spiff comparison summary.""" - successful_results = [r for r in results if r.success] - if len(successful_results) >= 2: - # FIX: Use max() for best score since higher is better in constraint solving! - best_result = max(successful_results, key=lambda r: calculate_total_score(r.final_score)) - fastest_result = min(successful_results, key=lambda r: r.solve_time_ms) - times = [r.solve_time_ms for r in successful_results] - avg_time = sum(times) / len(times) - - lines = [ - "", - "## ๐Ÿ† Performance Summary", - "", - "### ๐Ÿฅ‡ Champion Results", - "", - f"**๐Ÿ† Best Score Champion:** `{best_result.data_source}` ", - f"๐Ÿ“Š Score: `{format_score(best_result.final_score)}` ", - f"โฑ๏ธ Time: {best_result.solve_time_ms:,}ms ", - f"๐Ÿ”„ Iterations: {best_result.solver_iterations} ", - "", - f"**โšก Speed Champion:** `{fastest_result.data_source}` ", - f"โฑ๏ธ Time: {fastest_result.solve_time_ms:,}ms ", - f"๐Ÿ“Š Score: `{format_score(fastest_result.final_score)}` ", - f"๐Ÿ”„ Iterations: {fastest_result.solver_iterations} ", - "", - "### ๐Ÿ“ˆ Performance Analytics", - "", - f"๐ŸŽฏ **Average Solve Time:** {avg_time:.0f}ms ", - f"๐Ÿ“Š **Time Range:** {min(times):,}ms โ†’ {max(times):,}ms ", - f"๐Ÿš€ **Total Scenarios:** {len(successful_results)} ", - "" - ] - return lines - - return [] - - def create_cv_analysis(): - """Create coefficient of variation analysis for markdown.""" - grouped_results = group_results_by_scenario(results) - scenarios_with_multiple_runs = {k: v for k, v in grouped_results.items() if len(v) > 1} - - if not scenarios_with_multiple_runs: - return [] - - lines = [ - "", - "## ๐Ÿ“Š Consistency Analysis (Coefficient of Variation)", - "", - "### ๐Ÿ“ˆ Reliability Metrics", - "", - "| ๐Ÿท๏ธ Scenario | ๐Ÿ”„ Runs | โฑ๏ธ Avg Time | ๐Ÿ“Š CV Time | ๐ŸŽฏ Avg Score | ๐Ÿ“Š CV Score |", - "|-------------|---------|-------------|------------|--------------|-------------|" - ] - - for scenario_name, scenario_results in scenarios_with_multiple_runs.items(): - successful_runs = [r for r in scenario_results if r.success] - if len(successful_runs) < 2: - continue - - times = [r.solve_time_ms for r in successful_runs] - scores = [calculate_total_score(r.final_score) for r in successful_runs] - - avg_time = statistics.mean(times) - cv_time = calculate_coefficient_of_variation(times) - avg_score = statistics.mean(scores) - cv_score = calculate_coefficient_of_variation(scores) - - # Add consistency indicators - time_consistency = "๐ŸŸข" if cv_time < 10 else "๐ŸŸก" if cv_time < 25 else "๐Ÿ”ด" - score_consistency = "๐ŸŸข" if cv_score < 5 else "๐ŸŸก" if cv_score < 15 else "๐Ÿ”ด" - - lines.append(f"| **{scenario_name}** | {len(successful_runs)} | {avg_time:.0f}ms | {time_consistency} {cv_time:.1f}% | {avg_score:.0f} | {score_consistency} {cv_score:.1f}% |") - - lines.extend([ - "", - "### ๐Ÿ’ก CV Interpretation Guide", - "", - "- **๐ŸŸข Excellent consistency**: < 10% for time, < 5% for score", - "- **๐ŸŸก Good consistency**: < 25% for time, < 15% for score", - "- **๐Ÿ”ด High variability**: > 25% for time, > 15% for score", - "", - "> **Coefficient of Variation (CV)** measures relative variability: `CV = (Standard Deviation / Mean) ร— 100%`", - "" - ]) - - return lines - - def create_detailed_analysis(): - """Create spiff detailed constraint analysis.""" - lines = [ - "", - "## ๐Ÿ”ฌ Detailed Constraint Analysis", - "" - ] - - for result in results: - if result.success and result.analysis: - lines.extend([ - f"### ๐ŸŽฏ {result.data_source}", - "", - f"๐Ÿ“Š **Final Score:** `{format_score(result.final_score)}` ", - f"โฑ๏ธ **Solve Time:** {result.solve_time_ms:,}ms ", - f"๐Ÿ”„ **Iterations:** {result.solver_iterations} ", - "" - ]) - - # Add constraint breakdown from /analyze endpoint - constraints = result.analysis.get('constraints', []) - if constraints: - lines.extend([ - "#### ๐Ÿ” Constraint Breakdown", - "", - "| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact |", - "|---------------|----------|-----------|" - ]) - - for constraint in constraints: - name = constraint.get('name', 'Unknown') - score = format_score(constraint.get('score', {'hardScore': 0, 'mediumScore': 0, 'softScore': 0})) - - # Determine impact level with spiff indicators - constraint_score = constraint.get('score', {}) - - # Handle both dict and string score formats - if isinstance(constraint_score, dict): - hard_score = constraint_score.get('hardScore', 0) - medium_score = constraint_score.get('mediumScore', 0) - soft_score = constraint_score.get('softScore', 0) - - elif isinstance(constraint_score, str): - # Parse string format like "0hard/-5medium/-100soft" - try: - parts = constraint_score.split('/') - hard_score = int(parts[0].replace('hard', '')) if 'hard' in parts[0] else 0 - medium_score = int(parts[1].replace('medium', '')) if len(parts) > 1 and 'medium' in parts[1] else 0 - soft_score = int(parts[2].replace('soft', '')) if len(parts) > 2 and 'soft' in parts[2] else 0 - - except: - hard_score = medium_score = soft_score = 0 - - else: - hard_score = medium_score = soft_score = 0 - - # Determine impact based on parsed scores - match (hard_score, medium_score, soft_score): - case (h, _, _) if h != 0: - impact = "๐Ÿ”ด Critical" - - case (_, m, _) if m != 0: - impact = "๐ŸŸก Medium" - - case (_, _, s) if s < -1000: - impact = "๐ŸŸ  High" - - case (_, _, s) if s < -100: - impact = "๐ŸŸข Low" - - case _: - impact = "โšช Minimal" - - lines.append(f"| {name} | `{score}` | {impact} |") - - lines.append("") - - # Add any additional analysis data - if 'summary' in result.analysis: - lines.extend([ - f"๐Ÿ’ก **Summary:** {result.analysis['summary']}", - "" - ]) - - lines.append("---") - lines.append("") - - return lines - - click.echo(f"๐Ÿ“ Writing spiff markdown to: {output_file}") - - # Use closures to build output with maximum spiff - output_lines = [] - output_lines.extend(create_header()) - - successful_results = [r for r in results if r.success] - if not successful_results: - output_lines.extend([ - "## โŒ No Successful Runs", - "", - "๐Ÿšจ **All benchmark scenarios failed!** ", - "๐Ÿ”ง Check server connectivity and data format compatibility.", - "" - ]) - - else: - output_lines.extend(create_results_table()) - output_lines.extend(create_summary()) - output_lines.extend(create_cv_analysis()) - output_lines.extend(create_detailed_analysis()) - - # Write to file - try: - with open(output_file, 'w') as f: - f.write('\n'.join(output_lines)) - click.echo(f"โœ… Spiff benchmark report saved: {output_file}") - - except Exception as e: - click.echo(f"โŒ Error writing markdown file: {e}") - traceback.print_exc() - - -def calculate_coefficient_of_variation(values: List[float]) -> float: - """Calculate coefficient of variation (CV = std_dev / mean).""" - if not values or len(values) < 2: - return 0.0 - mean = statistics.mean(values) - if mean == 0: - return 0.0 - std_dev = statistics.stdev(values) - return (std_dev / mean) * 100 # Return as percentage - - -def group_results_by_scenario(results: List[BenchmarkResult]) -> Dict[str, List[BenchmarkResult]]: - """Group benchmark results by test scenario name.""" - grouped = defaultdict(list) - for result in results: - # Extract base scenario name (remove iteration numbers) - base_name = result.data_source.split(" (Run ")[0] - grouped[base_name].append(result) - return dict(grouped) - - -def print_results(results: List[BenchmarkResult], output_file: Optional[str] = None): - """Print benchmark results in a nice format and optionally save to markdown file.""" - def print_individual_results(): - """Print individual results.""" - click.echo("\n๐Ÿ“Š Individual Results:") - click.echo("-" * 40) - - for result in results: - click.echo(f"\n๐Ÿ”ธ {result.data_source}") - click.echo(f" Status: {'โœ… Success' if result.success else 'โŒ Failed'}") - - if result.success: - click.echo(f" Time: {result.solve_time_ms:,}ms") - click.echo(f" Score: {format_score(result.final_score)}") - click.echo(f" Iterations: {result.solver_iterations}") - else: - click.echo(f" Error: {result.error_message}") - click.echo(f" {'-' * 30}") - - def print_comparison_summary(): - """Print comparison summary.""" - successful_results = [r for r in results if r.success] - if len(successful_results) >= 2: - click.echo(f"\n๐Ÿ”„ COMPARISON SUMMARY") - click.echo("-" * 40) - - # Find best and fastest results - HIGHER score is better in constraint solving! - best_result = max(successful_results, key=lambda r: calculate_total_score(r.final_score)) - fastest_result = min(successful_results, key=lambda r: r.solve_time_ms) - - click.echo(f"๐Ÿ† Best Score: {best_result.data_source}") - click.echo(f" {format_score(best_result.final_score)} in {best_result.solve_time_ms:,}ms") - click.echo(f" {'-' * 30}") - - click.echo(f"\nโšก Fastest Solve: {fastest_result.data_source}") - click.echo(f" {fastest_result.solve_time_ms:,}ms with score {format_score(fastest_result.final_score)}") - click.echo(f" {'-' * 30}") - - # Speed comparison - times = [r.solve_time_ms for r in successful_results] - avg_time = sum(times) / len(times) - click.echo(f"\n๐Ÿ“ˆ Average solve time: {avg_time:.0f}ms") - - def print_cv_analysis(): - """Print coefficient of variation analysis for consistency.""" - click.echo(f"\n๐Ÿ“Š CONSISTENCY ANALYSIS (Coefficient of Variation)") - click.echo(f"------------------------------------------------") - - grouped_results = group_results_by_scenario(results) - scenarios_with_multiple_runs = {k: v for k, v in grouped_results.items() if len(v) > 1} - - if not scenarios_with_multiple_runs: - click.echo("โ— CV analysis unavailable - no scenarios with multiple runs") - return - - click.echo(f"{'Scenario':<50} {'Runs':<6} {'Avg Time':<12} {'CV Time':<10} {'Avg Score':<15} {'CV Score':<10}") - click.echo(f"{'-' * 48:<50} {'-' * 4:<6} {'-' * 10:<12} {'-' * 8:<10} {'-' * 13:<15} {'-' * 8:<10}") - - for scenario_name, scenario_results in scenarios_with_multiple_runs.items(): - successful_runs = [r for r in scenario_results if r.success] - if len(successful_runs) < 2: - continue - - times = [r.solve_time_ms for r in successful_runs] - scores = [calculate_total_score(r.final_score) for r in successful_runs] - - avg_time = statistics.mean(times) - cv_time = calculate_coefficient_of_variation(times) - avg_score = statistics.mean(scores) - cv_score = calculate_coefficient_of_variation(scores) - - # Add consistency indicators - time_consistency = "๐ŸŸข" if cv_time < 10 else "๐ŸŸก" if cv_time < 25 else "๐Ÿ”ด" - score_consistency = "๐ŸŸข" if cv_score < 5 else "๐ŸŸก" if cv_score < 15 else "๐Ÿ”ด" - - click.echo(f"{scenario_name[:48]:<50} {len(successful_runs):<6} {avg_time:>8.0f}ms {time_consistency} {cv_time:>6.1f}% {avg_score:>12.0f} {score_consistency} {cv_score:>6.1f}%") - - click.echo(f"\n๐Ÿ’ก CV Interpretation:") - click.echo(f" ๐ŸŸข Excellent consistency (< 10% for time, < 5% for score)") - click.echo(f" ๐ŸŸก Good consistency (< 25% for time, < 15% for score)") - click.echo(f" ๐Ÿ”ด High variability (> 25% for time, > 15% for score)") - - def print_constraint_analysis(): - """Print constraint analysis.""" - click.echo(f"\n๐Ÿ”ฌ CONSTRAINT ANALYSIS") - click.echo(f"----------------------------------------") - - # Find any Python backend result with analysis data - python_results = [r for r in results if ("Python Backend" in r.data_source or "Python2 Backend" in r.data_source) and r.analysis] - - if not python_results: - click.echo("โ— Constraint analysis unavailable - no Python backend results with analysis data") - return - - # Use the first available Python backend result with analysis - python_result = python_results[0] - click.echo(f"๐Ÿ“Š Analysis from: {python_result.data_source}") - click.echo(f"{'Constraint':<40} {'Score':<15}") - click.echo(f"{'-' * 38:<40} {'-' * 13:<15}") - - python_constraints = {c['name']: c for c in python_result.analysis.get('constraints', [])} - - for name in sorted(python_constraints.keys()): - python_constraint = python_constraints.get(name, {}) - python_score = format_score(python_constraint.get('score', {'hardScore': 0, 'mediumScore': 0, 'softScore': 0})) if python_constraint else "N/A" - click.echo(f"โ— {name:<38} {python_score:<15}") - - # Console output - click.echo("\n" + "="*80) - click.echo("๐Ÿ BENCHMARK RESULTS") - click.echo("="*80) - - successful_results = [r for r in results if r.success] - - if not successful_results: - click.echo("โŒ No successful benchmark runs!") - if output_file: - write_markdown_file(results, output_file) - return - - # Use closures for printing - print_individual_results() - print_comparison_summary() - print_cv_analysis() - print_constraint_analysis() - - # Write to markdown file if requested - if output_file: - write_markdown_file(results, output_file) - - -def run_benchmark(data: Dict, data_source: str, base_url: str) -> BenchmarkResult: - """Run a single benchmark with the given data.""" - def log_data_structure(): - """Log data structure details with spiff style.""" - click.echo(f" ๐ŸŽฏ Data Overview: {len(data.get('meetings', []))} meetings") - - if data.get('meetings'): - # Show sample meetings with correct mapping - sample_meetings = data['meetings'][:3] - total_req = 0 - total_pref = 0 - - for meeting in sample_meetings: - req_count = len(meeting.get('requiredAttendances', [])) - pref_count = len(meeting.get('preferredAttendances', [])) - total_req += req_count - total_pref += pref_count - - # Clean status indicators - req_status = "โœจ" if req_count > 0 else "โšช" - - click.echo(f" {req_status} Meeting {meeting.get('id')}: {req_count} required, {pref_count} preferred") - - # Show totals with appropriate format indicators - if "8080" in base_url: # Java backend - nested format - click.echo(f" ๐Ÿ“Š Total attendances: {total_req + total_pref} (nested in meetings)") - - else: # Python backend - top-level format - top_req = len(data.get('requiredAttendances', [])) - top_pref = len(data.get('preferredAttendances', [])) - click.echo(f" ๐Ÿ“Š Total attendances: {top_req + top_pref} ({top_req} required + {top_pref} preferred)") - - click.echo(f"\nโšก Running {data_source}...") - click.echo(f" {'-' * 50}") - - # Clean data structure overview - log_data_structure() - click.echo(f" {'-' * 50}") - - try: - # If calling the Python backend ensure a 30-second termination limit so iteration counts are comparable - if "8081" in base_url and "solverConfiguration" not in data: - import copy - data = copy.deepcopy(data) - data["solverConfiguration"] = { - "termination": { - "secondsSpentLimit": 30, - "unimprovedSecondsSpentLimit": 30 - } - } - - # Submit the job - response = requests.post(f"{base_url}/schedules", json=data) - - if response.status_code != 200: - error_message = f"Job submission failed: {response.status_code}" - try: - error_data = response.json() - if 'message' in error_data: - error_message += f" - {error_data['message']}" - except: - error_message += f" - Response: {response.text[:200]}" - return BenchmarkResult( - data_source=data_source, - job_id="", - solve_time_ms=0, - final_score={}, - solver_iterations=0, - success=False, - error_message=error_message - ) - - job_id = response.text.strip('"') - click.echo(f" ๐Ÿš€ Job launched: {job_id}") - - # Monitor solving progress with spiff progress indicators - iterations = 0 - last_score = None - solve_start_time = time.time() - progress_chars = ["โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "] - - while True: - time.sleep(0.5) # Check every 500ms - iterations += 1 - - response = requests.get(f"{base_url}/schedules/{job_id}/status") - if response.status_code != 200: - error_message = f"Status check failed: {response.status_code}" - try: - error_data = response.json() - - if 'message' in error_data: - error_message += f" - {error_data['message']}" - - except: - pass - - return BenchmarkResult( - data_source=data_source, - job_id=job_id, - solve_time_ms=0, - final_score={}, - solver_iterations=iterations, - success=False, - error_message=error_message - ) - - status_data = response.json() - solver_status = status_data.get('solverStatus') - current_score = status_data.get('score', {}) - - # Show progress with spiff spinner and score updates - if current_score != last_score and current_score: - spinner = progress_chars[iterations % len(progress_chars)] - click.echo(f" {spinner} Optimizing... {current_score}") - last_score = current_score - - if solver_status == "NOT_SOLVING": - solve_end_time = time.time() - solve_time_ms = int((solve_end_time - solve_start_time) * 1000) - - click.echo(f" โœ… Completed: {solve_time_ms:,}ms โ€ข {iterations} iterations โ€ข {format_score(current_score)}") - - return BenchmarkResult( - data_source=data_source, - job_id=job_id, - solve_time_ms=solve_time_ms, - final_score=current_score, - solver_iterations=iterations, - success=True - ) - - # Safety timeout (60 seconds) - if time.time() - solve_start_time > 60: - return BenchmarkResult( - data_source=data_source, - job_id=job_id, - solve_time_ms=0, - final_score=current_score, - solver_iterations=iterations, - success=False, - error_message="Timeout after 60 seconds" - ) - - except Exception as e: - return BenchmarkResult( - data_source=data_source, - job_id="", - solve_time_ms=0, - final_score={}, - solver_iterations=0, - success=False, - error_message=str(e) - ) - - -def analyze_schedule(data: Dict, base_url: str) -> Optional[Dict]: - """Analyze a schedule to get detailed constraint information.""" - try: - response = requests.put(f"{base_url}/schedules/analyze", json=data, timeout=30) - if response.status_code == 200: - return response.json() - - else: - click.echo(f" โŒ Analysis failed with status {response.status_code}: {response.text}") - return None - - except Exception as e: - click.echo(f" โŒ Analysis error: {e}") - return None - - -def format_score(score: Dict[str, int] | str) -> str: - """Format score for display.""" - def format_dict_score(score_dict): - """Format dictionary scores.""" - hard = score_dict.get('hardScore', 0) - medium = score_dict.get('mediumScore', 0) - soft = score_dict.get('softScore', 0) - return f"{hard}hard/{medium}medium/{soft}soft" - - def format_string_score(score_str): - """Format string scores.""" - return score_str - - return format_dict_score(score) if isinstance(score, dict) else format_string_score(score) - - -def calculate_total_score(score: Dict[str, int] | str) -> int: - """Calculate total weighted score.""" - def parse_score(): - """Parse score into components.""" - match score: - case str(): - try: - parts = score.split('/') - hard = int(parts[0].replace('hard', '')) - medium = int(parts[1].replace('medium', '')) - soft = int(parts[2].replace('soft', '')) - return hard, medium, soft - - except: - return 0, 0, 0 - - case dict(): - hard = score.get('hardScore', 0) - medium = score.get('mediumScore', 0) - soft = score.get('softScore', 0) - return hard, medium, soft - - case _: - return 0, 0, 0 - - def calculate_weighted_score(hard, medium, soft): - """Calculate weighted score.""" - match (hard, medium, soft): - case (h, _, _) if h != 0: - return h * 1000000 # Hard constraints are most critical - - case (_, m, _) if m != 0: - return m * 1000 # Medium constraints are second priority - - case (_, _, s): - return s # Soft constraints for optimization - - hard, medium, soft = parse_score() - return calculate_weighted_score(hard, medium, soft) - - -def prepare_for_analysis(solved_data: Dict, original_data: Dict) -> Dict: - """Prepare solved data for analysis by re-injecting and fixing original nested lists.""" - def fix_attendance_person(attendance, people_by_id): - """Fix person field in attendance.""" - if isinstance(attendance.get('person'), str): - person_id = attendance['person'] - attendance['person'] = people_by_id.get(person_id, {"id": person_id, "fullName": f"Person {person_id}"}) - - return attendance - - analysis_payload = solved_data.copy() - - # Create lookups - people_by_id = {p['id']: p for p in original_data.get('people', [])} - original_meetings = {m['id']: m for m in original_data.get('meetings', [])} - - # Re-inject and fix nested attendances for analysis - for meeting in analysis_payload.get('meetings', []): - if meeting['id'] in original_meetings: - original_meeting = original_meetings[meeting['id']] - - # Deep copy and fix required attendances - required_attendances = json.loads(json.dumps(original_meeting.get('requiredAttendances', []))) - - for attendance in required_attendances: - fix_attendance_person(attendance, people_by_id) - - meeting['requiredAttendances'] = required_attendances - - # Deep copy and fix preferred attendances - preferred_attendances = json.loads(json.dumps(original_meeting.get('preferredAttendances', []))) - - for attendance in preferred_attendances: - fix_attendance_person(attendance, people_by_id) - - meeting['preferredAttendances'] = preferred_attendances - - return analysis_payload - - -def convert_python_to_java_format(python_data: Dict) -> Dict: - """Convert Python-generated JSON to Java server format.""" - - def group_attendances_by_meeting(attendances): - """Group attendances by meeting ID.""" - grouped = {} - - for attendance in attendances: - meeting_id = attendance.get('meeting') - if meeting_id: - if meeting_id not in grouped: - grouped[meeting_id] = [] - - # Remove the meeting field and add to the meeting's list - attendance_copy = attendance.copy() - del attendance_copy['meeting'] - - # Convert person object to string ID if needed - if isinstance(attendance_copy.get('person'), dict): - attendance_copy['person'] = attendance_copy['person']['id'] - - grouped[meeting_id].append(attendance_copy) - - return grouped - - # Convert the data - converted = python_data.copy() - - # Set solver status if missing or null - if converted.get('solverStatus') is None: - converted['solverStatus'] = 'NOT_SOLVING' - - # Convert meetings to include nested attendances - if 'meetings' in converted: - # Group attendances by meeting - required_attendances_by_meeting = group_attendances_by_meeting(converted.get('requiredAttendances', [])) - preferred_attendances_by_meeting = group_attendances_by_meeting(converted.get('preferredAttendances', [])) - - # Add attendances to each meeting - for meeting in converted['meetings']: - meeting_id = meeting['id'] - meeting['requiredAttendances'] = required_attendances_by_meeting.get(meeting_id, []) - meeting['preferredAttendances'] = preferred_attendances_by_meeting.get(meeting_id, []) - - # Remove the top-level attendance arrays - if 'requiredAttendances' in converted: - del converted['requiredAttendances'] - - if 'preferredAttendances' in converted: - del converted['preferredAttendances'] - - # Remove the top-level attendance arrays - Java doesn't expect them - - return converted - - -def convert_java_to_python_format(java_data: Dict) -> Dict: - """Convert Java-generated JSON to Python server format - BULLETPROOF VERSION!""" - - # START FRESH - no copying, no references, pure reconstruction - python_data = {} - - # Copy scalar fields - NEVER PASS None VALUES! - # Python domain uses snake_case solver_status, not camelCase solverStatus! - solver_status_value = java_data.get("solverStatus") - - if solver_status_value is None or solver_status_value == "null": - python_data["solver_status"] = "NOT_SOLVING" - - else: - python_data["solver_status"] = solver_status_value - - if java_data.get("score") is not None: - python_data["score"] = java_data["score"] - - # Build person lookup for attendance resolution - people_lookup = {} - if 'people' in java_data: - for person in java_data['people']: - people_lookup[person['id']] = { - "id": person['id'], - "fullName": person.get('fullName', f"Person {person['id']}") - } - - # Copy all non-meeting arrays exactly as-is - for field in ['people', 'rooms', 'timeGrains', 'meetingAssignments']: - if field in java_data: - python_data[field] = json.loads(json.dumps(java_data[field])) - - # THE CRITICAL PART: Process meetings and attendances with ZERO data loss - python_data['meetings'] = [] - python_data['requiredAttendances'] = [] - python_data['preferredAttendances'] = [] - - if 'meetings' in java_data: - for java_meeting in java_data['meetings']: - # Build new meeting structure - python_meeting = { - "id": java_meeting['id'], - "topic": java_meeting.get('topic', f"Meeting {java_meeting['id']}"), - "durationInGrains": java_meeting.get('durationInGrains', 1), - "speakers": java_meeting.get('speakers') or [], - "content": java_meeting.get('content') or "", - "entireGroupMeeting": java_meeting.get('entireGroupMeeting', False), - "requiredAttendances": [], - "preferredAttendances": [] - } - - # Process REQUIRED attendances - extract every single one - java_required = java_meeting.get('requiredAttendances') or [] - for java_att in java_required: - # Extract person ID safely - if isinstance(java_att.get('person'), str): - person_id = java_att['person'] - - elif isinstance(java_att.get('person'), dict): - person_id = java_att['person']['id'] - - else: - continue # Skip malformed attendance - - # Get full person data - person_obj = people_lookup.get(person_id, { - "id": person_id, - "fullName": f"Person {person_id}" - }) - - # Create attendance record with ALL required fields - attendance_record = { - "id": java_att['id'], - "meeting": java_meeting['id'], - "person": person_obj - } - - # Add to BOTH places - this is crucial! - python_meeting['requiredAttendances'].append(attendance_record) - python_data['requiredAttendances'].append(attendance_record) - - # Process PREFERRED attendances - extract every single one - java_preferred = java_meeting.get('preferredAttendances') or [] - for java_att in java_preferred: - # Extract person ID safely - if isinstance(java_att.get('person'), str): - person_id = java_att['person'] - - elif isinstance(java_att.get('person'), dict): - person_id = java_att['person']['id'] - - else: - continue # Skip malformed attendance - - # Get full person data - person_obj = people_lookup.get(person_id, { - "id": person_id, - "fullName": f"Person {person_id}" - }) - - # Create attendance record with ALL required fields - attendance_record = { - "id": java_att['id'], - "meeting": java_meeting['id'], - "person": person_obj - } - - # Add to BOTH places - this is crucial! - python_meeting['preferredAttendances'].append(attendance_record) - python_data['preferredAttendances'].append(attendance_record) - - python_data['meetings'].append(python_meeting) - - return python_data - - -def get_demo_data(base_url: str) -> Optional[Dict]: - """Get fresh demo data from the server.""" - try: - response = requests.get(f"{base_url}/demo-data") - - if response.status_code == 200: - return response.json() - - else: - click.echo(f"โŒ Failed to get demo data: {response.status_code}") - return None - - except Exception as e: - click.echo(f"โŒ Error getting demo data: {e}") - return None - - -def check_server(base_url: str) -> bool: - """Check if the server is running.""" - try: - response = requests.get(f"{base_url}/demo-data", timeout=5) - return response.status_code == 200 - - except requests.ConnectionError: - return False - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/benchmarks/benchmark_vehicle_routing.py b/benchmarks/benchmark_vehicle_routing.py deleted file mode 100755 index a0a6774..0000000 --- a/benchmarks/benchmark_vehicle_routing.py +++ /dev/null @@ -1,645 +0,0 @@ -#!/usr/bin/env python3 - -import json, sys, time, traceback, click, requests -from dataclasses import dataclass -from typing import Dict, List, Optional -from collections import defaultdict -import statistics - -@dataclass -class BenchmarkConfig: - python_base_url: str = "http://127.0.0.1:8081" - python2_base_url: str = "http://127.0.0.1:8082" - java_base_url: str = "http://127.0.0.1:8080" - -@dataclass -class BenchmarkResult: - data_source: str - job_id: str - solve_time_ms: int - final_score: Dict[str, int] - solver_iterations: int - success: bool - analysis: Optional[Dict] = None - error_message: Optional[str] = None - -@click.command() -@click.option('--iterations', default=1, help='Number of iterations per test') -@click.option('--output-file', type=click.Path(), help='Output results to markdown file') -@click.option('--test', help='Run only a specific test by name') -def main(iterations, output_file, test): - """Vehicle Routing Benchmark Tool""" - config = BenchmarkConfig() - click.echo("\n๐Ÿ”ง Vehicle Routing Benchmark Tool") - click.echo("="*50) - - # Check all servers - python_server_ok = check_server(config.python_base_url) - python2_server_ok = check_server(config.python2_base_url) - java_server_ok = check_server(config.java_base_url) - - if not python_server_ok: - click.echo(f"โŒ Python server not running at {config.python_base_url}") - click.echo(f" Please start the Python server first with:") - click.echo(f" uvicorn src.vehicle_routing:app --host {config.python_base_url.split('//')[1].split(':')[0]} --port {config.python_base_url.split(':')[-1]}") - if not python2_server_ok: - click.echo(f"โŒ Python2 server not running at {config.python2_base_url}") - click.echo(f" Please start the Python2 server first with:") - click.echo(f" uvicorn src.vehicle_routing:app --host {config.python2_base_url.split('//')[1].split(':')[0]} --port {config.python2_base_url.split(':')[-1]}") - if not java_server_ok: - click.echo(f"โŒ Java server not running at {config.java_base_url}") - click.echo(f" Please start the Java server first") - - if not python_server_ok and not python2_server_ok and not java_server_ok: - if output_file: - click.echo(f"๐Ÿ“„ Writing error status to {output_file}") - with open(output_file, 'w') as f: - f.write("# Vehicle Routing Benchmark Results\n\n") - f.write(f"Generated on: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n") - f.write("## โŒ Server Error\n\n") - f.write("No servers running:\n") - f.write(f"- Python server: {config.python_base_url}\n") - f.write(f"- Python2 server: {config.python2_base_url}\n") - f.write(f"- Java server: {config.java_base_url}\n") - sys.exit(1) - - if python_server_ok: - click.echo(f"โœ… Python server is running at {config.python_base_url}") - if python2_server_ok: - click.echo(f"โœ… Python2 server is running at {config.python2_base_url}") - if java_server_ok: - click.echo(f"โœ… Java server is running at {config.java_base_url}") - click.echo() - - # Define test scenarios - test_scenarios = { - "Python Backend - Python Demo Data": { - "condition": python_server_ok, - "fetcher": lambda: get_demo_data(config.python_base_url), - "converter": None, - "backend": config.python_base_url - }, - "Python Backend - Java Demo Data": { - "condition": python_server_ok and java_server_ok, - "fetcher": lambda: get_demo_data(config.java_base_url), - "converter": lambda data: convert_java_to_python_format(data), - "backend": config.python_base_url - }, - "Python Backend (FAST) - Python Demo Data": { - "condition": python2_server_ok, - "fetcher": lambda: get_demo_data(config.python2_base_url), - "converter": None, - "backend": config.python2_base_url - }, - "Python Backend (FAST) - Java Demo Data": { - "condition": python2_server_ok and java_server_ok, - "fetcher": lambda: get_demo_data(config.java_base_url), - "converter": lambda data: convert_java_to_python_format(data), - "backend": config.python2_base_url - }, - "Java Backend - Java Demo Data": { - "condition": java_server_ok, - "fetcher": lambda: get_demo_data(config.java_base_url), - "converter": None, - "backend": config.java_base_url - }, - "Java Backend - Python Demo Data": { - "condition": java_server_ok and python_server_ok, - "fetcher": lambda: get_demo_data(config.python_base_url), - "converter": lambda data: convert_python_to_java_format(data), - "backend": config.java_base_url - } - } - - all_results = [] - if test: - if test in test_scenarios: - scenario = test_scenarios[test] - if scenario['condition']: - results = run_test_scenario( - test, - scenario['fetcher'], - scenario['converter'], - scenario['backend'], - iterations - ) - all_results.extend(results) - else: - click.echo(f"โš ๏ธ Skipping test '{test}' - required server not running.") - else: - click.echo(f"โŒ Unknown test: {test}") - click.echo(f" Available tests: {list(test_scenarios.keys())}") - else: - for name, scenario in test_scenarios.items(): - if scenario['condition']: - results = run_test_scenario( - name, - scenario['fetcher'], - scenario['converter'], - scenario['backend'], - iterations - ) - all_results.extend(results) - print_results(all_results, output_file) - -def run_test_scenario(test_name, data_fetcher, data_converter=None, target_backend=None, iterations=1): - click.echo(f"๐Ÿ“ฅ Testing {test_name}...") - click.echo("-" * 60) - def analyze_solution(data, backend, original_data=None): - click.echo(f" ๐Ÿ” Analyzing final solution...") - solved_data = data - if hasattr(result, 'job_id') and result.job_id: - try: - click.echo(f" ๐Ÿ“ฅ Fetching complete solution for job {result.job_id}...") - response = requests.get(f"{backend}/route-plans/{result.job_id}") - if response.status_code == 200: - solved_data = response.json() - click.echo(f" โœ… Retrieved solved route plan for analysis") - else: - click.echo(f" โš ๏ธ Could not retrieve solved route plan (status {response.status_code})") - click.echo(f" โš ๏ธ Using input data for analysis") - except Exception as e: - click.echo(f" โš ๏ธ Error retrieving solved route plan: {e}") - click.echo(f" โš ๏ธ Using input data for analysis") - analysis_payload = solved_data - if original_data: - analysis_payload = prepare_for_analysis(solved_data, original_data) - click.echo(f" ๐Ÿ”ฌ Calling /route-plans/analyze endpoint with solution data...") - analysis = analyze_route(analysis_payload, backend) - if analysis: - constraint_count = len(analysis.get('constraints', [])) - click.echo(f" ๐Ÿ“Š Analysis complete. Found {constraint_count} constraints.") - return analysis - else: - click.echo(f" โŒ Analysis failed") - return None - results = [] - for i in range(iterations): - click.echo(f"\n ๐Ÿ“ฅ Fetching demo data for iteration {i+1}...") - demo_data = data_fetcher() - if demo_data: - click.echo(f" โœ… Got demo data with {len(demo_data.get('visits', []))} visits") - original_demo_data = json.loads(json.dumps(demo_data)) - if data_converter: - demo_data = data_converter(demo_data) - result = run_benchmark(demo_data, f"{test_name} (Run {i+1})", target_backend) - if result.success: - if "Java Demo Data" in test_name and "Python Backend" in test_name: - analysis_result = analyze_solution(demo_data, target_backend, original_data=original_demo_data) - else: - analysis_result = analyze_solution(demo_data, target_backend) - result.analysis = analysis_result - results.append(result) - else: - click.echo(f" โŒ Failed to get demo data for iteration {i+1}") - results.append(BenchmarkResult( - data_source=f"{test_name} (Run {i+1})", - job_id="", - solve_time_ms=0, - final_score={}, - solver_iterations=0, - success=False, - error_message="Failed to get demo data" - )) - click.echo() # Add spacing after test - return results - -def run_benchmark(data: Dict, data_source: str, base_url: str) -> BenchmarkResult: - def log_data_structure(): - click.echo(f" ๐ŸŽฏ Data Overview: {len(data.get('visits', []))} visits, {len(data.get('vehicles', []))} vehicles") - click.echo(f"\nโšก Running {data_source}...") - click.echo(f" {'-' * 50}") - log_data_structure() - click.echo(f" {'-' * 50}") - try: - if "8081" in base_url and "solverConfiguration" not in data: - import copy - data = copy.deepcopy(data) - data["solverConfiguration"] = { - "termination": { - "secondsSpentLimit": 30, - "unimprovedSecondsSpentLimit": 30 - } - } - response = requests.post(f"{base_url}/route-plans", json=data) - if response.status_code != 200: - error_message = f"Job submission failed: {response.status_code}" - try: - error_data = response.json() - if 'message' in error_data: - error_message += f" - {error_data['message']}" - except: - error_message += f" - Response: {response.text[:200]}" - return BenchmarkResult( - data_source=data_source, - job_id="", - solve_time_ms=0, - final_score={}, - solver_iterations=0, - success=False, - error_message=error_message - ) - job_id = response.text.strip('"') - click.echo(f" ๐Ÿš€ Job launched: {job_id}") - iterations = 0 - last_score = None - solve_start_time = time.time() - progress_chars = ["โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "] - while True: - time.sleep(0.5) - iterations += 1 - response = requests.get(f"{base_url}/route-plans/{job_id}") - if response.status_code != 200: - error_message = f"Status check failed: {response.status_code}" - try: - error_data = response.json() - if 'message' in error_data: - error_message += f" - {error_data['message']}" - except: - pass - return BenchmarkResult( - data_source=data_source, - job_id=job_id, - solve_time_ms=0, - final_score={}, - solver_iterations=iterations, - success=False, - error_message=error_message - ) - status_data = response.json() - solver_status = status_data.get('solver_status') or status_data.get('solverStatus') - current_score = status_data.get('score', {}) - if current_score != last_score and current_score: - spinner = progress_chars[iterations % len(progress_chars)] - click.echo(f" {spinner} Optimizing... {current_score}") - last_score = current_score - if solver_status == "NOT_SOLVING": - solve_end_time = time.time() - solve_time_ms = int((solve_end_time - solve_start_time) * 1000) - click.echo(f" โœ… Completed: {solve_time_ms:,}ms โ€ข {iterations} iterations โ€ข {format_score(current_score)}") - return BenchmarkResult( - data_source=data_source, - job_id=job_id, - solve_time_ms=solve_time_ms, - final_score=current_score, - solver_iterations=iterations, - success=True - ) - if time.time() - solve_start_time > 60: - return BenchmarkResult( - data_source=data_source, - job_id=job_id, - solve_time_ms=0, - final_score=current_score, - solver_iterations=iterations, - success=False, - error_message="Timeout after 60 seconds" - ) - except Exception as e: - return BenchmarkResult( - data_source=data_source, - job_id="", - solve_time_ms=0, - final_score={}, - solver_iterations=0, - success=False, - error_message=str(e) - ) - -def analyze_route(data: Dict, base_url: str) -> Optional[Dict]: - try: - response = requests.put(f"{base_url}/route-plans/analyze", json=data, timeout=30) - if response.status_code == 200: - return response.json() - else: - click.echo(f" โŒ Analysis failed with status {response.status_code}: {response.text}") - return None - except Exception as e: - click.echo(f" โŒ Analysis error: {e}") - return None - -def format_score(score: Dict[str, int] | str) -> str: - def format_dict_score(score_dict): - hard = score_dict.get('hardScore', 0) - medium = score_dict.get('mediumScore', 0) - soft = score_dict.get('softScore', 0) - return f"{hard}hard/{medium}medium/{soft}soft" - def format_string_score(score_str): - return score_str - return format_dict_score(score) if isinstance(score, dict) else format_string_score(score) - -def calculate_total_score(score: Dict[str, int] | str) -> int: - def parse_score(): - match score: - case str(): - try: - parts = score.split('/') - hard = int(parts[0].replace('hard', '')) - medium = int(parts[1].replace('medium', '')) if len(parts) > 1 else 0 - soft = int(parts[2].replace('soft', '')) if len(parts) > 2 else 0 - return hard, medium, soft - except: - return 0, 0, 0 - case dict(): - hard = score.get('hardScore', 0) - medium = score.get('mediumScore', 0) - soft = score.get('softScore', 0) - return hard, medium, soft - case _: - return 0, 0, 0 - def calculate_weighted_score(hard, medium, soft): - match (hard, medium, soft): - case (h, _, _) if h != 0: - return h * 1000000 - case (_, m, _) if m != 0: - return m * 1000 - case (_, _, s): - return s - hard, medium, soft = parse_score() - return calculate_weighted_score(hard, medium, soft) - -def get_demo_data(base_url: str) -> Optional[Dict]: - try: - # For the FAST backend (8082), /demo-data returns the data directly - if base_url.endswith(":8082"): - response = requests.get(f"{base_url}/demo-data") - if response.status_code == 200: - return response.json() - else: - click.echo(f"โŒ Failed to get demo data: {response.status_code}") - return None - # For other backends, /demo-data returns a list of names, then fetch by name - response = requests.get(f"{base_url}/demo-data") - if response.status_code != 200: - click.echo(f"โŒ Failed to get demo data list: {response.status_code}") - return None - demo_list = response.json() - if not demo_list: - click.echo(f"โŒ No demo datasets available at {base_url}") - return None - dataset_id = demo_list[0] - response = requests.get(f"{base_url}/demo-data/{dataset_id}") - if response.status_code == 200: - return response.json() - else: - click.echo(f"โŒ Failed to get demo data: {response.status_code}") - return None - except Exception as e: - click.echo(f"โŒ Error getting demo data: {e}") - return None - -def check_server(base_url: str) -> bool: - try: - response = requests.get(f"{base_url}/demo-data", timeout=5) - return response.status_code == 200 - except requests.ConnectionError: - return False - -def convert_java_to_python_format(java_data: Dict) -> Dict: - # Stub: In real use, adapt field names/nesting as needed - # For now, assume the format is similar - return java_data - -def convert_python_to_java_format(python_data: Dict) -> Dict: - # Stub: In real use, adapt field names/nesting as needed - # For now, assume the format is similar - return python_data - -def prepare_for_analysis(solved_data: Dict, original_data: Dict) -> Dict: - # Stub: For vehicle routing, assume no special re-injection needed - return solved_data - -def write_markdown_file(results: List[BenchmarkResult], output_file: str): - """Write results to markdown file with maximum spiff.""" - def create_header(): - return [ - "# ๐Ÿšš Vehicle Routing Benchmark Results", - "", - f"๐Ÿ“… **Generated:** {time.strftime('%Y-%m-%d %H:%M:%S')} ", - f"๐Ÿš€ **Total Scenarios:** {len(results)} ", - f"โœ… **Successful Runs:** {len([r for r in results if r.success])} ", - "", - "---", - "" - ] - - def create_results_table(): - lines = [ - "## ๐Ÿ“Š Individual Results", - "", - "| ๐Ÿท๏ธ Scenario | ๐Ÿ“ˆ Status | โšก Time | ๐ŸŽฏ Score | ๐Ÿ”„ Iterations | โŒ Error |", - "|-------------|-----------|---------|----------|---------------|----------|" - ] - for result in results: - status_icon = "๐ŸŸข" if result.success else "๐Ÿ”ด" - status_text = "Success" if result.success else "Failed" - if result.success: - time_indicator = "๐Ÿš€" if result.solve_time_ms < 25000 else "โšก" if result.solve_time_ms < 35000 else "๐ŸŒ" - lines.append(f"| **{result.data_source}** | {status_icon} {status_text} | {time_indicator} {result.solve_time_ms:,}ms | `{format_score(result.final_score)}` | {result.solver_iterations} | - |") - else: - lines.append(f"| **{result.data_source}** | {status_icon} {status_text} | - | - | - | {result.error_message} |") - return lines - - def create_summary(): - successful_results = [r for r in results if r.success] - if len(successful_results) >= 2: - best_result = max(successful_results, key=lambda r: calculate_total_score(r.final_score)) - fastest_result = min(successful_results, key=lambda r: r.solve_time_ms) - times = [r.solve_time_ms for r in successful_results] - avg_time = sum(times) / len(times) - lines = [ - "", - "## ๐Ÿ† Performance Summary", - "", - "### ๐Ÿฅ‡ Champion Results", - "", - f"**๐Ÿ† Best Score Champion:** `{best_result.data_source}` ", - f"๐Ÿ“Š Score: `{format_score(best_result.final_score)}` ", - f"โฑ๏ธ Time: {best_result.solve_time_ms:,}ms ", - f"๐Ÿ”„ Iterations: {best_result.solver_iterations} ", - "", - f"**โšก Speed Champion:** `{fastest_result.data_source}` ", - f"โฑ๏ธ Time: {fastest_result.solve_time_ms:,}ms ", - f"๐Ÿ“Š Score: `{format_score(fastest_result.final_score)}` ", - f"๐Ÿ”„ Iterations: {fastest_result.solver_iterations} ", - "", - "### ๐Ÿ“ˆ Performance Analytics", - "", - f"๐ŸŽฏ **Average Solve Time:** {avg_time:.0f}ms ", - f"๐Ÿ“Š **Time Range:** {min(times):,}ms โ†’ {max(times):,}ms ", - f"๐Ÿš€ **Total Scenarios:** {len(successful_results)} ", - "" - ] - return lines - return [] - - def create_cv_analysis(): - grouped_results = group_results_by_scenario(results) - scenarios_with_multiple_runs = {k: v for k, v in grouped_results.items() if len(v) > 1} - if not scenarios_with_multiple_runs: - return [] - lines = [ - "", - "## ๐Ÿ“Š Consistency Analysis (Coefficient of Variation)", - "", - "### ๐Ÿ“ˆ Reliability Metrics", - "", - "| ๐Ÿท๏ธ Scenario | ๐Ÿ”„ Runs | โฑ๏ธ Avg Time | ๐Ÿ“Š CV Time | ๐ŸŽฏ Avg Score | ๐Ÿ“Š CV Score |", - "|-------------|---------|-------------|------------|--------------|-------------|" - ] - for scenario_name, scenario_results in scenarios_with_multiple_runs.items(): - successful_runs = [r for r in scenario_results if r.success] - if len(successful_runs) < 2: - continue - times = [r.solve_time_ms for r in successful_runs] - scores = [calculate_total_score(r.final_score) for r in successful_runs] - avg_time = statistics.mean(times) - cv_time = calculate_coefficient_of_variation(times) - avg_score = statistics.mean(scores) - cv_score = calculate_coefficient_of_variation(scores) - time_consistency = "๐ŸŸข" if cv_time < 10 else "๐ŸŸก" if cv_time < 25 else "๐Ÿ”ด" - score_consistency = "๐ŸŸข" if cv_score < 5 else "๐ŸŸก" if cv_score < 15 else "๐Ÿ”ด" - lines.append(f"| **{scenario_name}** | {len(successful_runs)} | {avg_time:.0f}ms | {time_consistency} {cv_time:.1f}% | {avg_score:.0f} | {score_consistency} {cv_score:.1f}% |") - lines.extend([ - "", - "### ๐Ÿ’ก CV Interpretation Guide", - "", - "- **๐ŸŸข Excellent consistency**: < 10% for time, < 5% for score", - "- **๐ŸŸก Good consistency**: < 25% for time, < 15% for score", - "- **๐Ÿ”ด High variability**: > 25% for time, > 15% for score", - "", - "> **Coefficient of Variation (CV)** measures relative variability: `CV = (Standard Deviation / Mean) ร— 100%`", - "" - ]) - return lines - - def create_detailed_analysis(): - lines = [ - "", - "## ๐Ÿ”ฌ Detailed Constraint Analysis", - "" - ] - for result in results: - if result.success and result.analysis: - lines.extend([ - f"### ๐ŸŽฏ {result.data_source}", - "", - f"๐Ÿ“Š **Final Score:** `{format_score(result.final_score)}` ", - f"โฑ๏ธ **Solve Time:** {result.solve_time_ms:,}ms ", - f"๐Ÿ”„ **Iterations:** {result.solver_iterations} ", - "" - ]) - constraints = result.analysis.get('constraints', []) - if constraints: - lines.extend([ - "#### ๐Ÿ” Constraint Breakdown", - "", - "| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact |", - "|---------------|----------|-----------|" - ]) - for constraint in constraints: - name = constraint.get('name', 'Unknown') - score = format_score(constraint.get('score', {'hardScore': 0, 'softScore': 0})) - constraint_score = constraint.get('score', {}) - if isinstance(constraint_score, dict): - hard_score = constraint_score.get('hardScore', 0) - soft_score = constraint_score.get('softScore', 0) - elif isinstance(constraint_score, str): - try: - parts = constraint_score.split('/') - hard_score = int(parts[0].replace('hard', '')) if 'hard' in parts[0] else 0 - soft_score = int(parts[1].replace('soft', '')) if len(parts) > 1 and 'soft' in parts[1] else 0 - except: - hard_score = soft_score = 0 - else: - hard_score = soft_score = 0 - if hard_score != 0: - impact = "๐Ÿ”ด Critical" - elif soft_score < -1000: - impact = "๐ŸŸ  High" - elif soft_score < -100: - impact = "๐ŸŸข Low" - else: - impact = "โšช Minimal" - lines.append(f"| {name} | `{score}` | {impact} |") - lines.append("") - if 'summary' in result.analysis: - lines.extend([ - f"๐Ÿ’ก **Summary:** {result.analysis['summary']}", - "" - ]) - lines.append("---") - lines.append("") - return lines - - click.echo(f"๐Ÿ“ Writing spiff markdown to: {output_file}") - output_lines = [] - output_lines.extend(create_header()) - successful_results = [r for r in results if r.success] - if not successful_results: - output_lines.extend([ - "## โŒ No Successful Runs", - "", - "๐Ÿšจ **All benchmark scenarios failed!** ", - "๐Ÿ”ง Check server connectivity and data format compatibility.", - "" - ]) - else: - output_lines.extend(create_results_table()) - output_lines.extend(create_summary()) - output_lines.extend(create_cv_analysis()) - output_lines.extend(create_detailed_analysis()) - try: - with open(output_file, 'w') as f: - f.write('\n'.join(output_lines)) - click.echo(f"โœ… Spiff benchmark report saved: {output_file}") - except Exception as e: - click.echo(f"โŒ Error writing markdown file: {e}") - traceback.print_exc() - -def print_results(results: List[BenchmarkResult], output_file: Optional[str] = None): - def print_individual_results(): - click.echo("\n๐Ÿ“Š Individual Results:") - click.echo("-" * 40) - for result in results: - click.echo(f"\n๐Ÿ”ธ {result.data_source}") - click.echo(f" Status: {'โœ… Success' if result.success else 'โŒ Failed'}") - if result.success: - click.echo(f" Time: {result.solve_time_ms:,}ms") - click.echo(f" Score: {format_score(result.final_score)}") - click.echo(f" Iterations: {result.solver_iterations}") - else: - click.echo(f" Error: {result.error_message}") - click.echo(f" {'-' * 30}") - click.echo("\n" + "="*80) - click.echo("๐Ÿ BENCHMARK RESULTS") - click.echo("="*80) - successful_results = [r for r in results if r.success] - if not successful_results: - click.echo("โŒ No successful benchmark runs!") - if output_file: - write_markdown_file(results, output_file) - return - print_individual_results() - if output_file: - write_markdown_file(results, output_file) - -def calculate_coefficient_of_variation(values: List[float]) -> float: - if not values or len(values) < 2: - return 0.0 - mean = statistics.mean(values) - if mean == 0: - return 0.0 - stdev = statistics.stdev(values) - return (stdev / mean) * 100 - -def group_results_by_scenario(results: List[BenchmarkResult]) -> Dict[str, List[BenchmarkResult]]: - grouped = defaultdict(list) - for result in results: - # Extract base scenario name (remove iteration numbers) - base_name = result.data_source.split(" (Run ")[0] - grouped[base_name].append(result) - return dict(grouped) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/benchmarks/report.md b/benchmarks/report.md deleted file mode 100644 index 441ed87..0000000 --- a/benchmarks/report.md +++ /dev/null @@ -1,257 +0,0 @@ -# Constraint Solver Performance Analysis: Python vs Java Implementation Comparison for the Meeting Scheduling and Vehicle Routing problems - -## Abstract - -This paper presents a comprehensive performance analysis of constraint solver implementations across Python and Java platforms, revealing a critical architectural issue that significantly impacts solution quality. Through systematic benchmarking of meeting scheduling problems, we discovered that Pydantic domain models in constraint solving contexts introduce validation complexity that interferes with constraint evaluation, leading to false positive constraint violations. Our analysis demonstrates that the architectural choice between Pydantic domain models and dataclass domain models has profound implications for constraint solver performance and solution quality. - -## 1. Introduction - -Constraint solving represents a critical computational challenge in optimization problems, where the choice of implementation architecture can significantly impact both performance and solution quality. This study examines the performance characteristics of Python and Java implementations of the Timefold constraint solver framework, specifically focusing on meeting scheduling problems. - -The research addresses a fundamental question: does the choice of domain model architecture (Pydantic vs dataclass) affect constraint solver performance and solution quality? Our findings reveal that architectural decisions have measurable impacts on constraint evaluation accuracy. - -## 2. Methodology - -### 2.1 Experimental Design - -We conducted a comprehensive benchmark study comparing three implementations: -- **Original Python Backend**: Uses a unified Pydantic domain model for both the API and the constraint solver -- **Python Backend (FAST)**: Uses dataclass domain models with clean separation of concerns -- **Java Backend**: Reference implementation (v1.24.0) using Java domain models - -### 2.2 Test Scenarios - -Each implementation was tested with both Python-generated and Java-generated demo data, creating six distinct test scenarios: -1. Python Backend - Python Demo Data -2. Python Backend - Java Demo Data -3. Python Backend (FAST) - Python Demo Data -4. Python Backend (FAST) - Java Demo Data -5. Java Backend - Java Demo Data -6. Java Backend - Python Demo Data - -### 2.3 Performance Metrics - -We measured: -- **Solution Quality**: Constraint violation scores (hard/medium/soft) -- **Performance**: Solve time and iteration count -- **Consistency**: Coefficient of variation across multiple runs -- **Reliability**: Success rate and error handling - -## 3. Results - -### 3.1 Performance Metrics - -The benchmark results across 60 total scenarios (10 iterations per test) revealed significant performance variations: - -| Rank | Implementation | Data Source | Avg Score | Avg Time (ms) | CV (%) | -|------|----------------|-------------|-----------|---------------|---------| -| 1 | Python Backend (FAST) | Java Demo | -3063soft | 30,290 | 0.2 | -| 2 | Java Backend | Java Demo | -3126soft | 30,144 | 0.0 | -| 3 | Python Backend | Java Demo | -3163soft | 30,542 | 0.7 | -| 4 | Python Backend (FAST) | Python Demo | -3316soft | 30,358 | 0.3 | -| 5 | Java Backend | Python Demo | -3316soft | 30,138 | 0.0 | -| 6 | Python Backend | Python Demo | -85000 | 30,525 | 0.7 | - -### 3.2 Critical Architectural Discovery - -**Primary Finding**: The original Python implementation exhibited two distinct performance issues that revealed fundamental architectural problems with Pydantic domain models in constraint solving contexts. - -**Issue 1: Constraint Evaluation Correctness** -The original Python implementation exhibited persistent medium constraint violations (-85medium) when processing Python demo data, while all other implementations achieved zero medium violations. This pattern revealed a fundamental correctness issue in constraint evaluation. - -**Issue 2: Optimization Completion** -The original Python implementation consistently failed to complete the full 60-iteration optimization cycle within the 30-second time limit, achieving only 46-58 iterations compared to the complete 60 iterations achieved by both the FAST Python and Java implementations. - -**Root Cause Analysis**: Both issues stem from using Pydantic domain models in constraint solving contexts. Pydantic models introduce validation complexity that interferes with constraint evaluation in two ways: -1. **Object Equality Complexity**: Pydantic's complex equality behavior affects Person object equality in attendance conflict constraints, leading to false positive violations -2. **Validation Overhead**: Pydantic validation during constraint solving slows iteration processing, preventing complete optimization cycles - -### 3.3 Constraint Violation Patterns - -**Medium Constraint Violations:** -- **Original Python Backend**: -85medium violations (Python demo data only) -- **All Other Implementations**: 0medium violations - -**Soft Constraint Violations:** -- Consistent across all implementations (-3000 to -3300 soft points) -- No significant architectural impact - -**Hard Constraint Violations:** -- None observed in any implementation - -### 3.4 Optimization Completion Analysis - -**Iteration Completion Patterns:** -- **Original Python Backend**: 46-58 iterations (incomplete optimization) -- **FAST Python Backend**: 60 iterations (complete optimization) -- **Java Backend**: 60 iterations (complete optimization) - -**Performance Implications:** -- **Original Python Backend** hits the 30-second time limit before completing optimization -- **Pydantic validation overhead** significantly slows iteration processing -- **Solution quality may be suboptimal** due to incomplete optimization cycles -- **FAST Python Backend** achieves full optimization within the same time limit - -### 3.5 Cross-Domain Validation: Vehicle Routing Results - -To ensure the generality of our findings, we extended our analysis to the Vehicle Routing quickstart, a fundamentally different optimization problem. The results were consistent with those from meeting scheduling: - -- **Original Python Backend (Pydantic):** - - Sometimes fails to complete all iterations (57โ€“59 vs 60 for FAST/Java) - - No hard constraint violations (due to lenient logic), but less consistent - - No improvement in solution quality over FAST - -- **Python Backend (FAST, dataclass):** - - Always completes all iterations (60/60) - - More consistent results (lower coefficient of variation) - - Solution quality matches or exceeds Java - -- **Java Backend:** - - Consistent, but not superior to Python FAST on solution quality - -**Conclusion:** -The architectural issues observed with Pydantic models in meeting scheduling are **replicated in vehicle routing**. The unified Pydantic model is empirically inferior across domains. - -## 4. Discussion - -### 4.1 Architectural Impact on Constraint Evaluation - -It is observable from our benchmark results that **domain model architecture directly impacts both constraint solver accuracy and performance**. The original Python implementation uses Pydantic domain models with complex validation logic, while the FAST implementation uses dataclass domain models with clean separation of concerns. - -**Pydantic Domain Models (Problematic):** -- Complex validation and serialization logic mixed with domain logic -- Validation context dependencies that can create inconsistent object instances -- Serialization/deserialization layers that interfere with object identity -- Constraint evaluation affected by validation complexity -- **Performance overhead** from validation during constraint solving -- **Object equality complexity** that interferes with constraint evaluation - -**Dataclass Domain Models (Optimal):** -- Simple, predictable objects without validation complexity -- Direct object references without serialization layers -- Consistent object identity throughout the solving process -- Clean separation between domain logic and serialization concerns -- **Minimal performance overhead** during constraint solving -- **Simple, predictable equality** for reliable constraint evaluation - -### 4.2 Constraint Evaluation Mechanism - -The medium violations specifically occur in attendance conflict constraints that use `Joiners.equal(lambda attendance: attendance.person)` to match attendance records for the same person. With Pydantic domain models, the validation complexity can create different Person instances for the same logical person, causing the constraint solver to fail to identify conflicts properly. - -### 4.3 Performance Implications - -The architectural choice has measurable performance implications across two dimensions: - -**Solution Quality Impact:** -- **Constraint Accuracy**: 85-point difference in medium constraint violations -- **Consistency**: Higher coefficient of variation in Pydantic-based implementations -- **Reliability**: Validation complexity introduces potential failure modes - -**Optimization Performance Impact:** -- **Iteration Completion**: Pydantic models prevent full optimization cycles (46-58 vs 60 iterations) -- **Processing Speed**: Validation overhead slows iteration processing -- **Solution Optimality**: Incomplete optimization may result in suboptimal solutions -- **Scalability**: Performance degradation becomes more severe with larger datasets - -### 4.4 Constraint Logic Analysis - -It is important to note that the constraint logic is intentionally identical between the two implementations, since both ports were developed by us using the same lambda expressions in their constraint definitions: - -```python -# Both implementations use identical constraint logic: -Joiners.equal(lambda attendance: attendance.person) -``` - -This eliminates alternative explanations for the performance differences: - -**Identical Constraint Logic:** -- **Lambda usage**: Exactly the same between implementations -- **Constraint definitions**: Identical algorithms -- **Joiners and filters**: Same logic patterns - -**Different Outcomes:** -- **Original implementation**: -85 medium violations -- **FAST implementation**: 0 medium violations - -and proves that the performance difference stems from **object equality behavior** rather than algorithmic differences. The issue is that Pydantic domain models create complex object equality that interferes with constraint evaluation, while dataclass domain models provide simple, predictable equality. - -**Object Equality Comparison:** -```python -# Original (Pydantic) - Complex equality with validation overhead -class Person(JsonDomainBase): - def __eq__(self, other) -> bool: - if not isinstance(other, Person): - return False - return self.id == other.id # Can fail due to validation complexity - -# FAST (Dataclass) - Simple, predictable equality -@dataclass -class Person: - id: str - full_name: str - # Automatic __eq__ based on all fields -``` - -The architectural choice of domain model type directly impacts both constraint evaluation accuracy and performance. The dual nature of the problem - constraint evaluation bugs and performance degradation - both stem from the same root cause: Pydantic's complexity in constraint solving contexts. - -### 4.5 Architectural Patterns and Domain-Specific Requirements - -**Domain-Specific Pattern Analysis:** - -The original implementation uses Pydantic domain models, which provide robust validation and serialization capabilities. However, constraint solving environments may benefit from simpler object structures that prioritize predictable behavior over extensive validation: - -- **Web API Context**: Benefits from extensive validation to ensure data integrity and security -- **Constraint Solving Context**: May benefit from simple, predictable objects for efficient comparison and manipulation - -**Single Responsibility Principle Violation:** - -The original approach combines multiple responsibilities in domain models: -- Domain representation for constraint solving -- API serialization/deserialization -- Input validation -- Object identity management - -While this approach works well in web API contexts, it may introduce complexity that affects constraint evaluation performance. - -**Separation of Concerns Architecture:** - -The FAST implementation demonstrates superior architecture through clear separation of concerns: -- **Domain Models**: Simple dataclasses focused on constraint solving -- **API Models**: Pydantic models handling serialization and validation at boundaries -- **Constraint Logic**: Operates on simple objects without validation overhead - -This separation eliminates the constraint evaluation issues while maintaining data integrity through boundary validation. - -### 4.5 Maintainability Implications - -The separated architecture approach may offer maintainability benefits: - -**Original Approach (Single Model):** -- Constraint solver bugs (-85 medium violations) -- Performance degradation (validation overhead during solving) -- Complex debugging (validation logic mixed with domain logic) -- Difficult refactoring (coupled responsibilities) - -**FAST Approach (Separated Models):** -- Correct constraint evaluation (0 medium violations) -- Optimal performance (no validation overhead during solving) -- Clear debugging (separated concerns) -- Easier refactoring (independent components) - -## 5. Conclusion - -This study demonstrates that **domain model architecture is a critical factor in constraint solver performance and accuracy**. The use of Pydantic domain models in constraint solving contexts introduces validation complexity that interferes with constraint evaluation, leading to false positive constraint violations. - -**Key Findings:** -1. Pydantic domain models cause both constraint evaluation bugs and performance degradation in constraint solving contexts -2. The architectural issues manifest in two ways: false positive constraint violations and incomplete optimization cycles -3. Dataclass domain models provide optimal performance and accuracy for constraint solving -4. Architectural separation of concerns is essential for constraint solver reliability in Python -5. The choice of domain model architecture has measurable impact on both solution quality and optimization completeness - -**Recommendations:** -1. Use dataclass domain models for constraint solving implementations -2. Reserve Pydantic models for API serialization/deserialization at system boundaries -3. Maintain clean separation between domain logic and validation logic -4. Consider both correctness and performance implications when designing constraint solver implementations -5. Monitor optimization completion rates as a key performance indicator diff --git a/benchmarks/results_meeting-scheduling.md b/benchmarks/results_meeting-scheduling.md deleted file mode 100644 index 421522d..0000000 --- a/benchmarks/results_meeting-scheduling.md +++ /dev/null @@ -1,1617 +0,0 @@ -# ๐ŸŽฏ Meeting Scheduler Benchmark Results - -๐Ÿ“… **Generated:** 2025-07-20 20:08:40 -๐Ÿš€ **Total Scenarios:** 60 -โœ… **Successful Runs:** 60 - ---- - -## ๐Ÿ“Š Individual Results - -| ๐Ÿท๏ธ Scenario | ๐Ÿ“ˆ Status | โšก Time | ๐ŸŽฏ Score | ๐Ÿ”„ Iterations | โŒ Error | -|-------------|-----------|---------|----------|---------------|----------| -| **Python Backend - Python Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,746ms | `0hard/-85medium/-4833soft` | 53 | - | -| **Python Backend - Python Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,559ms | `0hard/-85medium/-4910soft` | 51 | - | -| **Python Backend - Python Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,365ms | `0hard/-85medium/-4910soft` | 51 | - | -| **Python Backend - Python Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,103ms | `0hard/-85medium/-4986soft` | 50 | - | -| **Python Backend - Python Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,324ms | `0hard/-85medium/-4910soft` | 51 | - | -| **Python Backend - Python Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,653ms | `0hard/-85medium/-4910soft` | 52 | - | -| **Python Backend - Python Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,489ms | `0hard/-85medium/-4910soft` | 52 | - | -| **Python Backend - Python Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,659ms | `0hard/-85medium/-4910soft` | 52 | - | -| **Python Backend - Python Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,702ms | `0hard/-85medium/-4910soft` | 51 | - | -| **Python Backend - Python Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,647ms | `0hard/-85medium/-4910soft` | 51 | - | -| **Python Backend - Java Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,465ms | `0hard/0medium/-3155soft` | 56 | - | -| **Python Backend - Java Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,624ms | `0hard/0medium/-3168soft` | 57 | - | -| **Python Backend - Java Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,272ms | `0hard/0medium/-3208soft` | 56 | - | -| **Python Backend - Java Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,986ms | `0hard/0medium/-3142soft` | 58 | - | -| **Python Backend - Java Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,310ms | `0hard/0medium/-3135soft` | 56 | - | -| **Python Backend - Java Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,602ms | `0hard/0medium/-3125soft` | 57 | - | -| **Python Backend - Java Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,558ms | `0hard/0medium/-3109soft` | 48 | - | -| **Python Backend - Java Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,436ms | `0hard/0medium/-3186soft` | 46 | - | -| **Python Backend - Java Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,554ms | `0hard/0medium/-3151soft` | 47 | - | -| **Python Backend - Java Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,608ms | `0hard/0medium/-3248soft` | 47 | - | -| **Python Backend (FAST) - Python Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,262ms | `0hard/0medium/-3316soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,453ms | `0hard/0medium/-3316soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,263ms | `0hard/0medium/-3316soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,282ms | `0hard/0medium/-3316soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,308ms | `0hard/0medium/-3316soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,356ms | `0hard/0medium/-3316soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,341ms | `0hard/0medium/-3316soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,481ms | `0hard/0medium/-3316soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,383ms | `0hard/0medium/-3316soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,447ms | `0hard/0medium/-3316soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,270ms | `0hard/0medium/-3032soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,243ms | `0hard/0medium/-3109soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,287ms | `0hard/0medium/-3084soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,319ms | `0hard/0medium/-3053soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,381ms | `0hard/0medium/-3054soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,369ms | `0hard/0medium/-3057soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,331ms | `0hard/0medium/-3068soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,215ms | `0hard/0medium/-3041soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,255ms | `0hard/0medium/-3054soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,235ms | `0hard/0medium/-3079soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,144ms | `0hard/0medium/-3100soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,148ms | `0hard/0medium/-3131soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,141ms | `0hard/0medium/-3094soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,137ms | `0hard/0medium/-3141soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,165ms | `0hard/0medium/-3080soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,139ms | `0hard/0medium/-3184soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,139ms | `0hard/0medium/-3051soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,141ms | `0hard/0medium/-3150soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,137ms | `0hard/0medium/-3119soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,145ms | `0hard/0medium/-3209soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,137ms | `0hard/0medium/-3316soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,139ms | `0hard/0medium/-3316soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,133ms | `0hard/0medium/-3316soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,134ms | `0hard/0medium/-3316soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,136ms | `0hard/0medium/-3316soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,140ms | `0hard/0medium/-3316soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,140ms | `0hard/0medium/-3316soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,144ms | `0hard/0medium/-3316soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,139ms | `0hard/0medium/-3316soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,143ms | `0hard/0medium/-3316soft` | 60 | - | - -## ๐Ÿ† Performance Summary - -### ๐Ÿฅ‡ Champion Results - -**๐Ÿ† Best Score Champion:** `Python Backend (FAST) - Java Demo Data (Run 1)` -๐Ÿ“Š Score: `0hard/0medium/-3032soft` -โฑ๏ธ Time: 30,270ms -๐Ÿ”„ Iterations: 60 - -**โšก Speed Champion:** `Python Backend - Python Demo Data (Run 4)` -โฑ๏ธ Time: 30,103ms -๐Ÿ“Š Score: `0hard/-85medium/-4986soft` -๐Ÿ”„ Iterations: 50 - -### ๐Ÿ“ˆ Performance Analytics - -๐ŸŽฏ **Average Solve Time:** 30333ms -๐Ÿ“Š **Time Range:** 30,103ms โ†’ 30,986ms -๐Ÿš€ **Total Scenarios:** 60 - - -## ๐Ÿ“Š Consistency Analysis (Coefficient of Variation) - -### ๐Ÿ“ˆ Reliability Metrics - -| ๐Ÿท๏ธ Scenario | ๐Ÿ”„ Runs | โฑ๏ธ Avg Time | ๐Ÿ“Š CV Time | ๐ŸŽฏ Avg Score | ๐Ÿ“Š CV Score | -|-------------|---------|-------------|------------|--------------|-------------| -| **Python Backend - Python Demo Data** | 10 | 30525ms | ๐ŸŸข 0.7% | -85000 | ๐ŸŸข -0.0% | -| **Python Backend - Java Demo Data** | 10 | 30542ms | ๐ŸŸข 0.7% | -3163 | ๐ŸŸข -1.3% | -| **Python Backend (FAST) - Python Demo Data** | 10 | 30358ms | ๐ŸŸข 0.3% | -3316 | ๐ŸŸข -0.0% | -| **Python Backend (FAST) - Java Demo Data** | 10 | 30290ms | ๐ŸŸข 0.2% | -3063 | ๐ŸŸข -0.7% | -| **Java Backend - Java Demo Data** | 10 | 30144ms | ๐ŸŸข 0.0% | -3126 | ๐ŸŸข -1.5% | -| **Java Backend - Python Demo Data** | 10 | 30138ms | ๐ŸŸข 0.0% | -3316 | ๐ŸŸข -0.0% | - -### ๐Ÿ’ก CV Interpretation Guide - -- **๐ŸŸข Excellent consistency**: < 10% for time, < 5% for score -- **๐ŸŸก Good consistency**: < 25% for time, < 15% for score -- **๐Ÿ”ด High variability**: > 25% for time, > 15% for score - -> **Coefficient of Variation (CV)** measures relative variability: `CV = (Standard Deviation / Mean) ร— 100%` - - -## ๐Ÿ”ฌ Detailed Constraint Analysis - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/-85medium/-4833soft` -โฑ๏ธ **Solve Time:** 30,746ms -๐Ÿ”„ **Iterations:** 53 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-136soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-2021soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-6soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1470soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/-1200soft` | ๐ŸŸ  High | -| Preferred attendance conflict | `0hard/-34medium/0soft` | ๐ŸŸก Medium | -| Required and preferred attendance conflict | `0hard/-51medium/0soft` | ๐ŸŸก Medium | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/-85medium/-4910soft` -โฑ๏ธ **Solve Time:** 30,559ms -๐Ÿ”„ **Iterations:** 51 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1937soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-11soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1490soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| Preferred attendance conflict | `0hard/-28medium/0soft` | ๐ŸŸก Medium | -| Required and preferred attendance conflict | `0hard/-57medium/0soft` | ๐ŸŸก Medium | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/-85medium/-4910soft` -โฑ๏ธ **Solve Time:** 30,365ms -๐Ÿ”„ **Iterations:** 51 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1937soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-11soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1490soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| Preferred attendance conflict | `0hard/-28medium/0soft` | ๐ŸŸก Medium | -| Required and preferred attendance conflict | `0hard/-57medium/0soft` | ๐ŸŸก Medium | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/-85medium/-4986soft` -โฑ๏ธ **Solve Time:** 30,103ms -๐Ÿ”„ **Iterations:** 50 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-244soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1937soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-15soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1490soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| Preferred attendance conflict | `0hard/-30medium/0soft` | ๐ŸŸก Medium | -| Required and preferred attendance conflict | `0hard/-55medium/0soft` | ๐ŸŸก Medium | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/-85medium/-4910soft` -โฑ๏ธ **Solve Time:** 30,324ms -๐Ÿ”„ **Iterations:** 51 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1937soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-11soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1490soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| Preferred attendance conflict | `0hard/-28medium/0soft` | ๐ŸŸก Medium | -| Required and preferred attendance conflict | `0hard/-57medium/0soft` | ๐ŸŸก Medium | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/-85medium/-4910soft` -โฑ๏ธ **Solve Time:** 30,653ms -๐Ÿ”„ **Iterations:** 52 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1937soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-11soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1490soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| Preferred attendance conflict | `0hard/-28medium/0soft` | ๐ŸŸก Medium | -| Required and preferred attendance conflict | `0hard/-57medium/0soft` | ๐ŸŸก Medium | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/-85medium/-4910soft` -โฑ๏ธ **Solve Time:** 30,489ms -๐Ÿ”„ **Iterations:** 52 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1937soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-11soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1490soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| Preferred attendance conflict | `0hard/-28medium/0soft` | ๐ŸŸก Medium | -| Required and preferred attendance conflict | `0hard/-57medium/0soft` | ๐ŸŸก Medium | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/-85medium/-4910soft` -โฑ๏ธ **Solve Time:** 30,659ms -๐Ÿ”„ **Iterations:** 52 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1937soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-11soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1490soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| Preferred attendance conflict | `0hard/-28medium/0soft` | ๐ŸŸก Medium | -| Required and preferred attendance conflict | `0hard/-57medium/0soft` | ๐ŸŸก Medium | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/-85medium/-4910soft` -โฑ๏ธ **Solve Time:** 30,702ms -๐Ÿ”„ **Iterations:** 51 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1937soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-11soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1490soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| Preferred attendance conflict | `0hard/-28medium/0soft` | ๐ŸŸก Medium | -| Required and preferred attendance conflict | `0hard/-57medium/0soft` | ๐ŸŸก Medium | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/-85medium/-4910soft` -โฑ๏ธ **Solve Time:** 30,647ms -๐Ÿ”„ **Iterations:** 51 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1937soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-11soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1490soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| Preferred attendance conflict | `0hard/-28medium/0soft` | ๐ŸŸก Medium | -| Required and preferred attendance conflict | `0hard/-57medium/0soft` | ๐ŸŸก Medium | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3155soft` -โฑ๏ธ **Solve Time:** 30,465ms -๐Ÿ”„ **Iterations:** 56 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-152soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1702soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-1soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3168soft` -โฑ๏ธ **Solve Time:** 30,624ms -๐Ÿ”„ **Iterations:** 57 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-164soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1661soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-3soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1340soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3208soft` -โฑ๏ธ **Solve Time:** 30,272ms -๐Ÿ”„ **Iterations:** 56 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-162soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-4soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1290soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3142soft` -โฑ๏ธ **Solve Time:** 30,986ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1738soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1250soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3135soft` -โฑ๏ธ **Solve Time:** 30,310ms -๐Ÿ”„ **Iterations:** 56 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1651soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-2soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1310soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3125soft` -โฑ๏ธ **Solve Time:** 30,602ms -๐Ÿ”„ **Iterations:** 57 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-136soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1695soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-4soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1290soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3109soft` -โฑ๏ธ **Solve Time:** 30,558ms -๐Ÿ”„ **Iterations:** 48 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-152soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1616soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-1soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1340soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3186soft` -โฑ๏ธ **Solve Time:** 30,436ms -๐Ÿ”„ **Iterations:** 46 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-162soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1800soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-4soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1220soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3151soft` -โฑ๏ธ **Solve Time:** 30,554ms -๐Ÿ”„ **Iterations:** 47 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-152soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1714soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-5soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1280soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3248soft` -โฑ๏ธ **Solve Time:** 30,608ms -๐Ÿ”„ **Iterations:** 47 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-188soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1646soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-4soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,262ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,453ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,263ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,282ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,308ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,356ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,341ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,481ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,383ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,447ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3032soft` -โฑ๏ธ **Solve Time:** 30,270ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1598soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1280soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3109soft` -โฑ๏ธ **Solve Time:** 30,243ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1647soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1290soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3084soft` -โฑ๏ธ **Solve Time:** 30,287ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-144soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1710soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1230soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3053soft` -โฑ๏ธ **Solve Time:** 30,319ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-162soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1611soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1280soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3054soft` -โฑ๏ธ **Solve Time:** 30,381ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-136soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1668soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1250soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3057soft` -โฑ๏ธ **Solve Time:** 30,369ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-152soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1585soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1320soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3068soft` -โฑ๏ธ **Solve Time:** 30,331ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1644soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1270soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3041soft` -โฑ๏ธ **Solve Time:** 30,215ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-142soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1619soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1280soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3054soft` -โฑ๏ธ **Solve Time:** 30,255ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-162soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1622soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1270soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3079soft` -โฑ๏ธ **Solve Time:** 30,235ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-162soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1617soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3100soft` -โฑ๏ธ **Solve Time:** 30,144ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-136soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1714soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1250soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3131soft` -โฑ๏ธ **Solve Time:** 30,148ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-144soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1644soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-3soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1340soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3094soft` -โฑ๏ธ **Solve Time:** 30,141ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-144soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1685soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-5soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1260soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3141soft` -โฑ๏ธ **Solve Time:** 30,137ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-152soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1758soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-1soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1230soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3080soft` -โฑ๏ธ **Solve Time:** 30,165ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-144soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1666soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1270soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3184soft` -โฑ๏ธ **Solve Time:** 30,139ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-178soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1645soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-11soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1350soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3051soft` -โฑ๏ธ **Solve Time:** 30,139ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-152soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1598soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-1soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1300soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3150soft` -โฑ๏ธ **Solve Time:** 30,141ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-172soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1599soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-9soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1370soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3119soft` -โฑ๏ธ **Solve Time:** 30,137ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1597soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-8soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1360soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3209soft` -โฑ๏ธ **Solve Time:** 30,145ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-170soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1720soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/-9soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1310soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,137ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,139ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,133ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,134ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,136ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,140ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,140ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,144ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,139ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/0medium/-3316soft` -โฑ๏ธ **Solve Time:** 30,143ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| Assign larger rooms first | `0hard/0medium/-154soft` | ๐ŸŸข Low | -| Do all meetings as soon as possible | `0hard/0medium/-1752soft` | ๐ŸŸ  High | -| Room stability | `0hard/0medium/0soft` | โšช Minimal | -| Overlapping meetings | `0hard/0medium/-1410soft` | ๐ŸŸ  High | -| One TimeGrain break between two consecutive meetings | `0hard/0medium/0soft` | โšช Minimal | -| Preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required and preferred attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Don't go in overtime | `0hard/0medium/0soft` | โšช Minimal | -| Required attendance conflict | `0hard/0medium/0soft` | โšช Minimal | -| Required room capacity | `0hard/0medium/0soft` | โšช Minimal | -| Room conflict | `0hard/0medium/0soft` | โšช Minimal | -| Start and end on same day | `0hard/0medium/0soft` | โšช Minimal | - ---- diff --git a/benchmarks/results_vehicle-routing.md b/benchmarks/results_vehicle-routing.md deleted file mode 100644 index e7d13a7..0000000 --- a/benchmarks/results_vehicle-routing.md +++ /dev/null @@ -1,1077 +0,0 @@ -# ๐Ÿšš Vehicle Routing Benchmark Results - -๐Ÿ“… **Generated:** 2025-07-21 11:16:13 -๐Ÿš€ **Total Scenarios:** 60 -โœ… **Successful Runs:** 60 - ---- - -## ๐Ÿ“Š Individual Results - -| ๐Ÿท๏ธ Scenario | ๐Ÿ“ˆ Status | โšก Time | ๐ŸŽฏ Score | ๐Ÿ”„ Iterations | โŒ Error | -|-------------|-----------|---------|----------|---------------|----------| -| **Python Backend - Python Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,162ms | `0hard/-65994soft` | 59 | - | -| **Python Backend - Python Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,161ms | `0hard/-66303soft` | 59 | - | -| **Python Backend - Python Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,503ms | `0hard/-65994soft` | 60 | - | -| **Python Backend - Python Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,258ms | `0hard/-65994soft` | 59 | - | -| **Python Backend - Python Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,174ms | `0hard/-65994soft` | 58 | - | -| **Python Backend - Python Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,419ms | `0hard/-65994soft` | 58 | - | -| **Python Backend - Python Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,447ms | `0hard/-65994soft` | 58 | - | -| **Python Backend - Python Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,078ms | `0hard/-65994soft` | 57 | - | -| **Python Backend - Python Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,545ms | `0hard/-65994soft` | 58 | - | -| **Python Backend - Python Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,242ms | `0hard/-65994soft` | 57 | - | -| **Python Backend - Java Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,260ms | `0hard/-53175soft` | 57 | - | -| **Python Backend - Java Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,353ms | `0hard/-53175soft` | 59 | - | -| **Python Backend - Java Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,376ms | `0hard/-53175soft` | 59 | - | -| **Python Backend - Java Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,047ms | `0hard/-53175soft` | 58 | - | -| **Python Backend - Java Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,204ms | `0hard/-53175soft` | 58 | - | -| **Python Backend - Java Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,046ms | `0hard/-53175soft` | 58 | - | -| **Python Backend - Java Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,160ms | `0hard/-53175soft` | 58 | - | -| **Python Backend - Java Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,486ms | `0hard/-53175soft` | 59 | - | -| **Python Backend - Java Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,060ms | `0hard/-53175soft` | 58 | - | -| **Python Backend - Java Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,058ms | `0hard/-53175soft` | 58 | - | -| **Python Backend (FAST) - Python Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,282ms | `0hard/-65994soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,312ms | `0hard/-65994soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,282ms | `0hard/-65994soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,407ms | `0hard/-65994soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,429ms | `0hard/-66303soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,393ms | `0hard/-65994soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,413ms | `0hard/-65994soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,411ms | `0hard/-65994soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,360ms | `0hard/-66303soft` | 60 | - | -| **Python Backend (FAST) - Python Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,411ms | `0hard/-66303soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,388ms | `0hard/-53175soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,321ms | `0hard/-53175soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,329ms | `0hard/-53175soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,256ms | `0hard/-53175soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,275ms | `0hard/-53175soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,301ms | `0hard/-53175soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,294ms | `0hard/-53175soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,302ms | `0hard/-53175soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,289ms | `0hard/-53175soft` | 60 | - | -| **Python Backend (FAST) - Java Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,287ms | `0hard/-53175soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,203ms | `0hard/-79422soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,198ms | `0hard/-79422soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,199ms | `0hard/-79422soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,205ms | `0hard/-79422soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,193ms | `0hard/-79422soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,189ms | `0hard/-79422soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,194ms | `0hard/-79422soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,194ms | `0hard/-79422soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,190ms | `0hard/-79422soft` | 60 | - | -| **Java Backend - Java Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,183ms | `0hard/-79422soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 1)** | ๐ŸŸข Success | โšก 30,194ms | `-75hard/-95113soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 2)** | ๐ŸŸข Success | โšก 30,198ms | `-75hard/-95113soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 3)** | ๐ŸŸข Success | โšก 30,187ms | `-75hard/-95113soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 4)** | ๐ŸŸข Success | โšก 30,196ms | `-75hard/-95113soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 5)** | ๐ŸŸข Success | โšก 30,198ms | `-75hard/-95113soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 6)** | ๐ŸŸข Success | โšก 30,204ms | `-75hard/-95113soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 7)** | ๐ŸŸข Success | โšก 30,194ms | `-75hard/-95113soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 8)** | ๐ŸŸข Success | โšก 30,205ms | `-75hard/-95113soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 9)** | ๐ŸŸข Success | โšก 30,193ms | `-75hard/-95113soft` | 60 | - | -| **Java Backend - Python Demo Data (Run 10)** | ๐ŸŸข Success | โšก 30,191ms | `-75hard/-95113soft` | 60 | - | - -## ๐Ÿ† Performance Summary - -### ๐Ÿฅ‡ Champion Results - -**๐Ÿ† Best Score Champion:** `Python Backend - Python Demo Data (Run 1)` -๐Ÿ“Š Score: `0hard/-65994soft` -โฑ๏ธ Time: 30,162ms -๐Ÿ”„ Iterations: 59 - -**โšก Speed Champion:** `Python Backend - Java Demo Data (Run 6)` -โฑ๏ธ Time: 30,046ms -๐Ÿ“Š Score: `0hard/-53175soft` -๐Ÿ”„ Iterations: 58 - -### ๐Ÿ“ˆ Performance Analytics - -๐ŸŽฏ **Average Solve Time:** 30261ms -๐Ÿ“Š **Time Range:** 30,046ms โ†’ 30,545ms -๐Ÿš€ **Total Scenarios:** 60 - - -## ๐Ÿ“Š Consistency Analysis (Coefficient of Variation) - -### ๐Ÿ“ˆ Reliability Metrics - -| ๐Ÿท๏ธ Scenario | ๐Ÿ”„ Runs | โฑ๏ธ Avg Time | ๐Ÿ“Š CV Time | ๐ŸŽฏ Avg Score | ๐Ÿ“Š CV Score | -|-------------|---------|-------------|------------|--------------|-------------| -| **Python Backend - Python Demo Data** | 10 | 30299ms | ๐ŸŸข 0.5% | 0 | ๐ŸŸข 0.0% | -| **Python Backend - Java Demo Data** | 10 | 30205ms | ๐ŸŸข 0.5% | 0 | ๐ŸŸข 0.0% | -| **Python Backend (FAST) - Python Demo Data** | 10 | 30370ms | ๐ŸŸข 0.2% | 0 | ๐ŸŸข 0.0% | -| **Python Backend (FAST) - Java Demo Data** | 10 | 30304ms | ๐ŸŸข 0.1% | 0 | ๐ŸŸข 0.0% | -| **Java Backend - Java Demo Data** | 10 | 30195ms | ๐ŸŸข 0.0% | 0 | ๐ŸŸข 0.0% | -| **Java Backend - Python Demo Data** | 10 | 30196ms | ๐ŸŸข 0.0% | 0 | ๐ŸŸข 0.0% | - -### ๐Ÿ’ก CV Interpretation Guide - -- **๐ŸŸข Excellent consistency**: < 10% for time, < 5% for score -- **๐ŸŸก Good consistency**: < 25% for time, < 15% for score -- **๐Ÿ”ด High variability**: > 25% for time, > 15% for score - -> **Coefficient of Variation (CV)** measures relative variability: `CV = (Standard Deviation / Mean) ร— 100%` - - -## ๐Ÿ”ฌ Detailed Constraint Analysis - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,162ms -๐Ÿ”„ **Iterations:** 59 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/-66303soft` -โฑ๏ธ **Solve Time:** 30,161ms -๐Ÿ”„ **Iterations:** 59 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-66303soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,503ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,258ms -๐Ÿ”„ **Iterations:** 59 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,174ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,419ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,447ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,078ms -๐Ÿ”„ **Iterations:** 57 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,545ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Python Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,242ms -๐Ÿ”„ **Iterations:** 57 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,260ms -๐Ÿ”„ **Iterations:** 57 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,353ms -๐Ÿ”„ **Iterations:** 59 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,376ms -๐Ÿ”„ **Iterations:** 59 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,047ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,204ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,046ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,160ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,486ms -๐Ÿ”„ **Iterations:** 59 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,060ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend - Java Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,058ms -๐Ÿ”„ **Iterations:** 58 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,282ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,312ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,282ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,407ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/-66303soft` -โฑ๏ธ **Solve Time:** 30,429ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-66303soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,393ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,413ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/-65994soft` -โฑ๏ธ **Solve Time:** 30,411ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-65994soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/-66303soft` -โฑ๏ธ **Solve Time:** 30,360ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-66303soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Python Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/-66303soft` -โฑ๏ธ **Solve Time:** 30,411ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-66303soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,388ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,321ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,329ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,256ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,275ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,301ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,294ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,302ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,289ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Python Backend (FAST) - Java Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/-53175soft` -โฑ๏ธ **Solve Time:** 30,287ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-53175soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `0hard/-79422soft` -โฑ๏ธ **Solve Time:** 30,203ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-79422soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `0hard/-79422soft` -โฑ๏ธ **Solve Time:** 30,198ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-79422soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `0hard/-79422soft` -โฑ๏ธ **Solve Time:** 30,199ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-79422soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `0hard/-79422soft` -โฑ๏ธ **Solve Time:** 30,205ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-79422soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `0hard/-79422soft` -โฑ๏ธ **Solve Time:** 30,193ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-79422soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `0hard/-79422soft` -โฑ๏ธ **Solve Time:** 30,189ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-79422soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `0hard/-79422soft` -โฑ๏ธ **Solve Time:** 30,194ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-79422soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `0hard/-79422soft` -โฑ๏ธ **Solve Time:** 30,194ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-79422soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `0hard/-79422soft` -โฑ๏ธ **Solve Time:** 30,190ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-79422soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Java Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `0hard/-79422soft` -โฑ๏ธ **Solve Time:** 30,183ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-79422soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `0hard/0soft` | โšช Minimal | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 1) - -๐Ÿ“Š **Final Score:** `-75hard/-95113soft` -โฑ๏ธ **Solve Time:** 30,194ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-95113soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `-75hard/0soft` | ๐Ÿ”ด Critical | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 2) - -๐Ÿ“Š **Final Score:** `-75hard/-95113soft` -โฑ๏ธ **Solve Time:** 30,198ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-95113soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `-75hard/0soft` | ๐Ÿ”ด Critical | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 3) - -๐Ÿ“Š **Final Score:** `-75hard/-95113soft` -โฑ๏ธ **Solve Time:** 30,187ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-95113soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `-75hard/0soft` | ๐Ÿ”ด Critical | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 4) - -๐Ÿ“Š **Final Score:** `-75hard/-95113soft` -โฑ๏ธ **Solve Time:** 30,196ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-95113soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `-75hard/0soft` | ๐Ÿ”ด Critical | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 5) - -๐Ÿ“Š **Final Score:** `-75hard/-95113soft` -โฑ๏ธ **Solve Time:** 30,198ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-95113soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `-75hard/0soft` | ๐Ÿ”ด Critical | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 6) - -๐Ÿ“Š **Final Score:** `-75hard/-95113soft` -โฑ๏ธ **Solve Time:** 30,204ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-95113soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `-75hard/0soft` | ๐Ÿ”ด Critical | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 7) - -๐Ÿ“Š **Final Score:** `-75hard/-95113soft` -โฑ๏ธ **Solve Time:** 30,194ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-95113soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `-75hard/0soft` | ๐Ÿ”ด Critical | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 8) - -๐Ÿ“Š **Final Score:** `-75hard/-95113soft` -โฑ๏ธ **Solve Time:** 30,205ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-95113soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `-75hard/0soft` | ๐Ÿ”ด Critical | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 9) - -๐Ÿ“Š **Final Score:** `-75hard/-95113soft` -โฑ๏ธ **Solve Time:** 30,193ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-95113soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `-75hard/0soft` | ๐Ÿ”ด Critical | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- - -### ๐ŸŽฏ Java Backend - Python Demo Data (Run 10) - -๐Ÿ“Š **Final Score:** `-75hard/-95113soft` -โฑ๏ธ **Solve Time:** 30,191ms -๐Ÿ”„ **Iterations:** 60 - -#### ๐Ÿ” Constraint Breakdown - -| ๐Ÿท๏ธ Constraint | ๐Ÿ“Š Score | ๐ŸŽฏ Impact | -|---------------|----------|-----------| -| minimizeTravelTime | `0hard/-95113soft` | ๐ŸŸ  High | -| serviceFinishedAfterMaxEndTime | `-75hard/0soft` | ๐Ÿ”ด Critical | -| vehicleCapacity | `0hard/0soft` | โšช Minimal | - ---- diff --git a/fast/employee-scheduling-fast/src/employee_scheduling/__init__.py b/fast/employee-scheduling-fast/src/employee_scheduling/__init__.py deleted file mode 100644 index 776b489..0000000 --- a/fast/employee-scheduling-fast/src/employee_scheduling/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -import uvicorn - -from .rest_api import app - - -def main(): - config = uvicorn.Config("employee_scheduling:app", - port=8080, - log_config="logging.conf", - use_colors=True) - server = uvicorn.Server(config) - server.run() - - -if __name__ == "__main__": - main() diff --git a/fast/employee-scheduling-fast/src/employee_scheduling/constraints.py b/fast/employee-scheduling-fast/src/employee_scheduling/constraints.py deleted file mode 100644 index 66c41dd..0000000 --- a/fast/employee-scheduling-fast/src/employee_scheduling/constraints.py +++ /dev/null @@ -1,134 +0,0 @@ -from timefold.solver.score import (constraint_provider, ConstraintFactory, Joiners, HardSoftDecimalScore, ConstraintCollectors) -from datetime import datetime, date - -from .domain import Employee, Shift - - -def get_minute_overlap(shift1: Shift, shift2: Shift) -> int: - return (min(shift1.end, shift2.end) - max(shift1.start, shift2.start)).total_seconds() // 60 - - -def is_overlapping_with_date(shift: Shift, dt: date) -> bool: - return shift.start.date() == dt or shift.end.date() == dt - - -def overlapping_in_minutes(first_start_datetime: datetime, first_end_datetime: datetime, - second_start_datetime: datetime, second_end_datetime: datetime) -> int: - latest_start = max(first_start_datetime, second_start_datetime) - earliest_end = min(first_end_datetime, second_end_datetime) - delta = (earliest_end - latest_start).total_seconds() / 60 - return max(0, delta) - - -def get_shift_overlapping_duration_in_minutes(shift: Shift, dt: date) -> int: - overlap = 0 - start_date_time = datetime.combine(dt, datetime.max.time()) - end_date_time = datetime.combine(dt, datetime.min.time()) - overlap += overlapping_in_minutes(start_date_time, end_date_time, shift.start, shift.end) - return overlap - - -@constraint_provider -def define_constraints(constraint_factory: ConstraintFactory): - return [ - # Hard constraints - required_skill(constraint_factory), - no_overlapping_shifts(constraint_factory), - at_least_10_hours_between_two_shifts(constraint_factory), - one_shift_per_day(constraint_factory), - unavailable_employee(constraint_factory), - # Soft constraints - undesired_day_for_employee(constraint_factory), - desired_day_for_employee(constraint_factory), - balance_employee_shift_assignments(constraint_factory) - ] - - -def required_skill(constraint_factory: ConstraintFactory): - return (constraint_factory.for_each(Shift) - .filter(lambda shift: shift.required_skill not in shift.employee.skills) - .penalize(HardSoftDecimalScore.ONE_HARD) - .as_constraint("Missing required skill") - ) - - -def no_overlapping_shifts(constraint_factory: ConstraintFactory): - return (constraint_factory - .for_each_unique_pair(Shift, - Joiners.equal(lambda shift: shift.employee.name), - Joiners.overlapping(lambda shift: shift.start, lambda shift: shift.end)) - .penalize(HardSoftDecimalScore.ONE_HARD, get_minute_overlap) - .as_constraint("Overlapping shift") - ) - - -def at_least_10_hours_between_two_shifts(constraint_factory: ConstraintFactory): - return (constraint_factory - .for_each(Shift) - .join(Shift, - Joiners.equal(lambda shift: shift.employee.name), - Joiners.less_than_or_equal(lambda shift: shift.end, lambda shift: shift.start) - ) - .filter(lambda first_shift, second_shift: - (second_shift.start - first_shift.end).total_seconds() // (60 * 60) < 10) - .penalize(HardSoftDecimalScore.ONE_HARD, - lambda first_shift, second_shift: - 600 - ((second_shift.start - first_shift.end).total_seconds() // 60)) - .as_constraint("At least 10 hours between 2 shifts") - ) - - -def one_shift_per_day(constraint_factory: ConstraintFactory): - return (constraint_factory - .for_each_unique_pair(Shift, - Joiners.equal(lambda shift: shift.employee.name), - Joiners.equal(lambda shift: shift.start.date())) - .penalize(HardSoftDecimalScore.ONE_HARD) - .as_constraint("Max one shift per day") - ) - - -def unavailable_employee(constraint_factory: ConstraintFactory): - return (constraint_factory.for_each(Shift) - .join(Employee, Joiners.equal(lambda shift: shift.employee, lambda employee: employee)) - .flatten_last(lambda employee: employee.unavailable_dates) - .filter(lambda shift, unavailable_date: is_overlapping_with_date(shift, unavailable_date)) - .penalize(HardSoftDecimalScore.ONE_HARD, - lambda shift, unavailable_date: get_shift_overlapping_duration_in_minutes(shift, - unavailable_date)) - .as_constraint("Unavailable employee") - ) - - -def undesired_day_for_employee(constraint_factory: ConstraintFactory): - return (constraint_factory.for_each(Shift) - .join(Employee, Joiners.equal(lambda shift: shift.employee, lambda employee: employee)) - .flatten_last(lambda employee: employee.undesired_dates) - .filter(lambda shift, undesired_date: is_overlapping_with_date(shift, undesired_date)) - .penalize(HardSoftDecimalScore.ONE_SOFT, - lambda shift, undesired_date: get_shift_overlapping_duration_in_minutes(shift, undesired_date)) - .as_constraint("Undesired day for employee") - ) - - -def desired_day_for_employee(constraint_factory: ConstraintFactory): - return (constraint_factory.for_each(Shift) - .join(Employee, Joiners.equal(lambda shift: shift.employee, lambda employee: employee)) - .flatten_last(lambda employee: employee.desired_dates) - .filter(lambda shift, desired_date: is_overlapping_with_date(shift, desired_date)) - .reward(HardSoftDecimalScore.ONE_SOFT, - lambda shift, desired_date: get_shift_overlapping_duration_in_minutes(shift, desired_date)) - .as_constraint("Desired day for employee") - ) - - -def balance_employee_shift_assignments(constraint_factory: ConstraintFactory): - return (constraint_factory.for_each(Shift) - .group_by(lambda shift: shift.employee, ConstraintCollectors.count()) - .complement(Employee, lambda e: 0) # Include all employees which are not assigned to any shift. - .group_by(ConstraintCollectors.load_balance(lambda employee, shift_count: employee, - lambda employee, shift_count: shift_count)) - .penalize_decimal(HardSoftDecimalScore.ONE_SOFT, lambda load_balance: load_balance.unfairness()) - .as_constraint("Balance employee shift assignments") - ) - diff --git a/fast/employee-scheduling-fast/src/employee_scheduling/demo_data.py b/fast/employee-scheduling-fast/src/employee_scheduling/demo_data.py deleted file mode 100644 index 4c8bc1f..0000000 --- a/fast/employee-scheduling-fast/src/employee_scheduling/demo_data.py +++ /dev/null @@ -1,228 +0,0 @@ -from datetime import date, datetime, time, timedelta -from itertools import product -from enum import Enum -from random import Random -from typing import Generator -from dataclasses import dataclass, field - -from .domain import * - - -class DemoData(Enum): - SMALL = 'SMALL' - LARGE = 'LARGE' - - -@dataclass(frozen=True, kw_only=True) -class CountDistribution: - count: int - weight: float - - -def counts(distributions: tuple[CountDistribution, ...]) -> tuple[int, ...]: - return tuple(distribution.count for distribution in distributions) - - -def weights(distributions: tuple[CountDistribution, ...]) -> tuple[float, ...]: - return tuple(distribution.weight for distribution in distributions) - - -@dataclass(kw_only=True) -class DemoDataParameters: - locations: tuple[str, ...] - required_skills: tuple[str, ...] - optional_skills: tuple[str, ...] - days_in_schedule: int - employee_count: int - optional_skill_distribution: tuple[CountDistribution, ...] - shift_count_distribution: tuple[CountDistribution, ...] - availability_count_distribution: tuple[CountDistribution, ...] - random_seed: int = field(default=37) - - -demo_data_to_parameters: dict[DemoData, DemoDataParameters] = { - DemoData.SMALL: DemoDataParameters( - locations=("Ambulatory care", "Critical care", "Pediatric care"), - required_skills=("Doctor", "Nurse"), - optional_skills=("Anaesthetics", "Cardiology"), - days_in_schedule=14, - employee_count=15, - optional_skill_distribution=( - CountDistribution(count=1, weight=3), - CountDistribution(count=2, weight=1) - ), - shift_count_distribution=( - CountDistribution(count=1, weight=0.9), - CountDistribution(count=2, weight=0.1) - ), - availability_count_distribution=( - CountDistribution(count=1, weight=4), - CountDistribution(count=2, weight=3), - CountDistribution(count=3, weight=2), - CountDistribution(count=4, weight=1) - ), - random_seed=37 - ), - - DemoData.LARGE: DemoDataParameters( - locations=("Ambulatory care", - "Neurology", - "Critical care", - "Pediatric care", - "Surgery", - "Radiology", - "Outpatient"), - required_skills=("Doctor", "Nurse"), - optional_skills=("Anaesthetics", "Cardiology", "Radiology"), - days_in_schedule=28, - employee_count=50, - optional_skill_distribution=( - CountDistribution(count=1, weight=3), - CountDistribution(count=2, weight=1) - ), - shift_count_distribution=( - CountDistribution(count=1, weight=0.5), - CountDistribution(count=2, weight=0.3), - CountDistribution(count=3, weight=0.2) - ), - availability_count_distribution=( - CountDistribution(count=5, weight=4), - CountDistribution(count=10, weight=3), - CountDistribution(count=15, weight=2), - CountDistribution(count=20, weight=1) - ), - random_seed=37 - ) -} - - -FIRST_NAMES = ("Amy", "Beth", "Carl", "Dan", "Elsa", "Flo", "Gus", "Hugo", "Ivy", "Jay") -LAST_NAMES = ("Cole", "Fox", "Green", "Jones", "King", "Li", "Poe", "Rye", "Smith", "Watt") -SHIFT_LENGTH = timedelta(hours=8) -MORNING_SHIFT_START_TIME = time(hour=6, minute=0) -DAY_SHIFT_START_TIME = time(hour=9, minute=0) -AFTERNOON_SHIFT_START_TIME = time(hour=14, minute=0) -NIGHT_SHIFT_START_TIME = time(hour=22, minute=0) - -SHIFT_START_TIMES_COMBOS = ( - (MORNING_SHIFT_START_TIME, AFTERNOON_SHIFT_START_TIME), - (MORNING_SHIFT_START_TIME, AFTERNOON_SHIFT_START_TIME, NIGHT_SHIFT_START_TIME), - (MORNING_SHIFT_START_TIME, DAY_SHIFT_START_TIME, AFTERNOON_SHIFT_START_TIME, NIGHT_SHIFT_START_TIME), -) - - -location_to_shift_start_time_list_map = dict() - - -def earliest_monday_on_or_after(target_date: date): - """ - Returns the date of the next given weekday after - the given date. For example, the date of next Monday. - - NB: if it IS the day we're looking for, this returns 0. - consider then doing onDay(foo, day + 1). - """ - days = (7 - target_date.weekday()) % 7 - return target_date + timedelta(days=days) - - -def generate_demo_data(demo_data_or_parameters: DemoData | DemoDataParameters) -> EmployeeSchedule: - global location_to_shift_start_time_list_map, demo_data_to_parameters - if isinstance(demo_data_or_parameters, DemoData): - parameters = demo_data_to_parameters[demo_data_or_parameters] - else: - parameters = demo_data_or_parameters - - start_date = earliest_monday_on_or_after(date.today()) - random = Random(parameters.random_seed) - shift_template_index = 0 - for location in parameters.locations: - location_to_shift_start_time_list_map[location] = SHIFT_START_TIMES_COMBOS[shift_template_index] - shift_template_index = (shift_template_index + 1) % len(SHIFT_START_TIMES_COMBOS) - - name_permutations = [f'{first_name} {last_name}' - for first_name, last_name in product(FIRST_NAMES, LAST_NAMES)] - random.shuffle(name_permutations) - - employees = [] - for i in range(parameters.employee_count): - count, = random.choices(population=counts(parameters.optional_skill_distribution), - weights=weights(parameters.optional_skill_distribution)) - skills = [] - skills += random.sample(parameters.optional_skills, count) - skills += random.sample(parameters.required_skills, 1) - employees.append( - Employee(name=name_permutations[i], - skills=set(skills)) - ) - - shifts: list[Shift] = [] - - def id_generator(): - current_id = 0 - while True: - yield str(current_id) - current_id += 1 - - ids = id_generator() - - for i in range(parameters.days_in_schedule): - count, = random.choices(population=counts(parameters.availability_count_distribution), - weights=weights(parameters.availability_count_distribution)) - employees_with_availabilities_on_day = random.sample(employees, count) - current_date = start_date + timedelta(days=i) - for employee in employees_with_availabilities_on_day: - rand_num = random.randint(0, 2) - if rand_num == 0: - employee.unavailable_dates.add(current_date) - elif rand_num == 1: - employee.undesired_dates.add(current_date) - elif rand_num == 2: - employee.desired_dates.add(current_date) - shifts += generate_shifts_for_day(parameters, current_date, random, ids) - - shift_count = 0 - for shift in shifts: - shift.id = str(shift_count) - shift_count += 1 - - return EmployeeSchedule( - employees=employees, - shifts=shifts - ) - - -def generate_shifts_for_day(parameters: DemoDataParameters, current_date: date, random: Random, - ids: Generator[str, any, any]) -> list[Shift]: - global location_to_shift_start_time_list_map - shifts = [] - for location in parameters.locations: - shift_start_times = location_to_shift_start_time_list_map[location] - for start_time in shift_start_times: - shift_start_date_time = datetime.combine(current_date, start_time) - shift_end_date_time = shift_start_date_time + SHIFT_LENGTH - shifts += generate_shifts_for_timeslot(parameters, shift_start_date_time, shift_end_date_time, - location, random, ids) - - return shifts - - -def generate_shifts_for_timeslot(parameters: DemoDataParameters, timeslot_start: datetime, timeslot_end: datetime, - location: str, random: Random, ids: Generator[str, any, any]) -> list[Shift]: - shift_count, = random.choices(population=counts(parameters.shift_count_distribution), - weights=weights(parameters.shift_count_distribution)) - - shifts = [] - for i in range(shift_count): - if random.random() >= 0.5: - required_skill = random.choice(parameters.required_skills) - else: - required_skill = random.choice(parameters.optional_skills) - shifts.append(Shift( - id=next(ids), - start=timeslot_start, - end=timeslot_end, - location=location, - required_skill=required_skill)) - - return shifts diff --git a/fast/employee-scheduling-fast/src/employee_scheduling/rest_api.py b/fast/employee-scheduling-fast/src/employee_scheduling/rest_api.py deleted file mode 100644 index 8ff49cd..0000000 --- a/fast/employee-scheduling-fast/src/employee_scheduling/rest_api.py +++ /dev/null @@ -1,56 +0,0 @@ -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles -from uuid import uuid4 -from dataclasses import replace - -from .domain import EmployeeSchedule, EmployeeScheduleModel -from .converters import ( - schedule_to_model, model_to_schedule -) -from .demo_data import DemoData, generate_demo_data -from .solver import solver_manager, solution_manager - -app = FastAPI(docs_url='/q/swagger-ui') -data_sets: dict[str, EmployeeSchedule] = {} - - -@app.get("/demo-data") -async def demo_data_list() -> list[DemoData]: - return [e for e in DemoData] - - -@app.get("/demo-data/{dataset_id}", response_model_exclude_none=True) -async def get_demo_data(dataset_id: str) -> EmployeeScheduleModel: - demo_data = getattr(DemoData, dataset_id) - domain_schedule = generate_demo_data(demo_data) - return schedule_to_model(domain_schedule) - - -@app.get("/schedules/{problem_id}", response_model_exclude_none=True) -async def get_timetable(problem_id: str) -> EmployeeScheduleModel: - schedule = data_sets[problem_id] - updated_schedule = replace(schedule, solver_status=solver_manager.get_solver_status(problem_id)) - return schedule_to_model(updated_schedule) - - -def update_schedule(problem_id: str, schedule: EmployeeSchedule): - global data_sets - data_sets[problem_id] = schedule - - -@app.post("/schedules") -async def solve_timetable(schedule_model: EmployeeScheduleModel) -> str: - job_id = str(uuid4()) - schedule = model_to_schedule(schedule_model) - data_sets[job_id] = schedule - solver_manager.solve_and_listen(job_id, schedule, - lambda solution: update_schedule(job_id, solution)) - return job_id - - -@app.delete("/schedules/{problem_id}") -async def stop_solving(problem_id: str) -> None: - solver_manager.terminate_early(problem_id) - - -app.mount("/", StaticFiles(directory="static", html=True), name="static") diff --git a/fast/employee-scheduling-fast/tests/test_constraints.py b/fast/employee-scheduling-fast/tests/test_constraints.py deleted file mode 100644 index dfa0e83..0000000 --- a/fast/employee-scheduling-fast/tests/test_constraints.py +++ /dev/null @@ -1,217 +0,0 @@ -from timefold.solver.test import ConstraintVerifier - -from employee_scheduling.domain import * -from employee_scheduling.constraints import * - -from datetime import date, datetime, time, timedelta - -DAY_1 = date(2021, 2, 1) -DAY_3 = date(2021, 2, 3) -DAY_START_TIME = datetime.combine(DAY_1, time(9, 0)) -DAY_END_TIME = datetime.combine(DAY_1, time(17, 0)) -AFTERNOON_START_TIME = datetime.combine(DAY_1, time(13, 0)) -AFTERNOON_END_TIME = datetime.combine(DAY_1, time(21, 0)) - -constraint_verifier = ConstraintVerifier.build(define_constraints, EmployeeSchedule, Shift) - - -def test_required_skill(): - employee = Employee(name="Amy") - (constraint_verifier.verify_that(required_skill) - .given(employee, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee)) - .penalizes(1)) - - employee = Employee(name="Beth", skills={"Skill"}) - (constraint_verifier.verify_that(required_skill) - .given(employee, - Shift(id="2", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee)) - .penalizes(0)) - - -def test_overlapping_shifts(): - employee1 = Employee(name="Amy") - employee2 = Employee(name="Beth") - (constraint_verifier.verify_that(no_overlapping_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=DAY_START_TIME, end=DAY_END_TIME, location="Location 2", required_skill="Skill", employee=employee1)) - .penalizes_by(timedelta(hours=8) // timedelta(minutes=1))) - - (constraint_verifier.verify_that(no_overlapping_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=DAY_START_TIME, end=DAY_END_TIME, location="Location 2", required_skill="Skill", employee=employee2)) - .penalizes(0)) - - (constraint_verifier.verify_that(no_overlapping_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=AFTERNOON_START_TIME, end=AFTERNOON_END_TIME, location="Location 2", required_skill="Skill", employee=employee1)) - .penalizes_by(timedelta(hours=4) // timedelta(minutes=1))) - - -def test_one_shift_per_day(): - employee1 = Employee(name="Amy") - employee2 = Employee(name="Beth") - (constraint_verifier.verify_that(no_overlapping_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=DAY_START_TIME, end=DAY_END_TIME, location="Location 2", required_skill="Skill", employee=employee1)) - .penalizes(1)) - - (constraint_verifier.verify_that(no_overlapping_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=DAY_START_TIME, end=DAY_END_TIME, location="Location 2", required_skill="Skill", employee=employee2)) - .penalizes(0)) - - (constraint_verifier.verify_that(no_overlapping_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=AFTERNOON_START_TIME, end=AFTERNOON_END_TIME, location="Location 2", required_skill="Skill", employee=employee1)) - .penalizes(1)) - - (constraint_verifier.verify_that(no_overlapping_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=DAY_START_TIME + timedelta(days=1), end=DAY_END_TIME + timedelta(days=1), location="Location 2", required_skill="Skill", employee=employee1)) - .penalizes(0)) - - -def test_at_least_10_hours_between_shifts(): - employee1 = Employee(name="Amy") - employee2 = Employee(name="Beth") - - (constraint_verifier.verify_that(at_least_10_hours_between_two_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=AFTERNOON_END_TIME, end=DAY_START_TIME + timedelta(days=1), location="Location 2", required_skill="Skill", employee=employee1)) - .penalizes_by(360)) - - (constraint_verifier.verify_that(at_least_10_hours_between_two_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=DAY_END_TIME, end=DAY_START_TIME + timedelta(days=1), location="Location 2", required_skill="Skill", employee=employee1)) - .penalizes_by(600)) - - (constraint_verifier.verify_that(at_least_10_hours_between_two_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_END_TIME, end=DAY_START_TIME + timedelta(days=1), location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=DAY_START_TIME, end=DAY_END_TIME, location="Location 2", required_skill="Skill", employee=employee1)) - .penalizes_by(600)) - - (constraint_verifier.verify_that(at_least_10_hours_between_two_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=DAY_END_TIME + timedelta(hours=10), end=DAY_START_TIME + timedelta(days=1), location="Location 2", required_skill="Skill", employee=employee1)) - .penalizes(0)) - - (constraint_verifier.verify_that(at_least_10_hours_between_two_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=AFTERNOON_END_TIME, end=DAY_START_TIME + timedelta(days=1), location="Location 2", required_skill="Skill", employee=employee2)) - .penalizes(0)) - - (constraint_verifier.verify_that(no_overlapping_shifts) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=DAY_START_TIME + timedelta(days=1), end=DAY_END_TIME + timedelta(days=1), location="Location 2", required_skill="Skill", employee=employee1)) - .penalizes(0)) - - -def test_unavailable_employee(): - employee1 = Employee(name="Amy", unavailable_dates={DAY_1, DAY_3}) - employee2 = Employee(name="Beth") - - (constraint_verifier.verify_that(unavailable_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1)) - .penalizes_by(timedelta(hours=8) // timedelta(minutes=1))) - - (constraint_verifier.verify_that(unavailable_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME - timedelta(days=1), end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1)) - .penalizes_by(timedelta(hours=17) // timedelta(minutes=1))) - - (constraint_verifier.verify_that(unavailable_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME + timedelta(days=1), end=DAY_END_TIME + timedelta(days=1), location="Location", required_skill="Skill", employee=employee1)) - .penalizes(0)) - - (constraint_verifier.verify_that(unavailable_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee2)) - .penalizes(0)) - - -def test_undesired_day_for_employee(): - employee1 = Employee(name="Amy", undesired_dates={DAY_1, DAY_3}) - employee2 = Employee(name="Beth") - - (constraint_verifier.verify_that(undesired_day_for_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1)) - .penalizes_by(timedelta(hours=8) // timedelta(minutes=1))) - - (constraint_verifier.verify_that(undesired_day_for_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME - timedelta(days=1), end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1)) - .penalizes_by(timedelta(hours=17) // timedelta(minutes=1))) - - (constraint_verifier.verify_that(undesired_day_for_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME + timedelta(days=1), end=DAY_END_TIME + timedelta(days=1), location="Location", required_skill="Skill", employee=employee1)) - .penalizes(0)) - - (constraint_verifier.verify_that(undesired_day_for_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee2)) - .penalizes(0)) - - -def test_desired_day_for_employee(): - employee1 = Employee(name="Amy", desired_dates={DAY_1, DAY_3}) - employee2 = Employee(name="Beth") - - (constraint_verifier.verify_that(desired_day_for_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1)) - .rewards_with(timedelta(hours=8) // timedelta(minutes=1))) - - (constraint_verifier.verify_that(desired_day_for_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME - timedelta(days=1), end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1)) - .rewards_with(timedelta(hours=17) // timedelta(minutes=1))) - - (constraint_verifier.verify_that(desired_day_for_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME + timedelta(days=1), end=DAY_END_TIME + timedelta(days=1), location="Location", required_skill="Skill", employee=employee1)) - .rewards(0)) - - (constraint_verifier.verify_that(desired_day_for_employee) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee2)) - .rewards(0)) - -def test_balance_employee_shift_assignments(): - employee1 = Employee(name="Amy", desired_dates={DAY_1, DAY_3}) - employee2 = Employee(name="Beth") - - # No employees have shifts assigned; the schedule is perfectly balanced. - (constraint_verifier.verify_that(balance_employee_shift_assignments) - .given(employee1, employee2) - .penalizes_by(0)) - - # Only one employee has shifts assigned; the schedule is less balanced. - (constraint_verifier.verify_that(balance_employee_shift_assignments) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1)) - .penalizes_by_more_than(0)) - - # Every employee has a shift assigned; the schedule is once again perfectly balanced. - (constraint_verifier.verify_that(balance_employee_shift_assignments) - .given(employee1, employee2, - Shift(id="1", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee1), - Shift(id="2", start=DAY_START_TIME, end=DAY_END_TIME, location="Location", required_skill="Skill", employee=employee2)) - .penalizes_by(0)) diff --git a/fast/employee-scheduling-fast/tests/test_feasible.py b/fast/employee-scheduling-fast/tests/test_feasible.py deleted file mode 100644 index f47661c..0000000 --- a/fast/employee-scheduling-fast/tests/test_feasible.py +++ /dev/null @@ -1,37 +0,0 @@ -from timefold.solver import SolverFactory -from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, TerminationConfig, Duration, - TerminationCompositionStyle) - -from employee_scheduling.rest_api import app -from employee_scheduling.domain import EmployeeScheduleModel -from employee_scheduling.converters import model_to_schedule - -from fastapi.testclient import TestClient -from time import sleep -from pytest import fail - -client = TestClient(app) - - -def test_feasible(): - demo_data_response = client.get("/demo-data/SMALL") - assert demo_data_response.status_code == 200 - - job_id_response = client.post("/schedules", json=demo_data_response.json()) - assert job_id_response.status_code == 200 - job_id = job_id_response.text[1:-1] - - ATTEMPTS = 1_000 - for _ in range(ATTEMPTS): - sleep(0.1) - schedule_response = client.get(f"/schedules/{job_id}") - schedule_json = schedule_response.json() - schedule_model = EmployeeScheduleModel.model_validate(schedule_json) - schedule = model_to_schedule(schedule_model) - if schedule.score is not None and schedule.score.is_feasible: - stop_solving_response = client.delete(f"/schedules/{job_id}") - assert stop_solving_response.status_code == 200 - return - - client.delete(f"/schedules/{job_id}") - fail('solution is not feasible') diff --git a/fast/meeting-scheduling-fast/README.adoc b/fast/meeting-scheduling-fast/README.adoc deleted file mode 100644 index 3232c24..0000000 --- a/fast/meeting-scheduling-fast/README.adoc +++ /dev/null @@ -1,87 +0,0 @@ -= Meeting Scheduling (Python) - -Schedule meetings between employees, where each meeting has a topic, duration, required and preferred attendees. - -* <> -* <> -* <> - -[[prerequisites]] -== Prerequisites - -. Install https://www.python.org/downloads/[Python 3.11 or 3.12] - -. Install JDK 17+, for example with https://sdkman.io[Sdkman]: -+ ----- -$ sdk install java ----- - -[[run]] -== Run the application - -. Git clone the timefold-solver-python repo and navigate to this directory: -+ -[source, shell] ----- -$ git clone https://github.com/TimefoldAI/timefold-solver-python.git -... -$ cd timefold-solver-python/quickstarts/meeting-scheduling ----- - -. Create a virtual environment -+ -[source, shell] ----- -$ python -m venv .venv ----- - -. Activate the virtual environment -+ -[source, shell] ----- -$ . .venv/bin/activate ----- - -. Install the application -+ -[source, shell] ----- -$ pip install -e . ----- - -. Run the application -+ -[source, shell] ----- -$ run-app ----- - -. Visit http://localhost:8000 in your browser. - -. Click on the *Solve* button. - - -[[test]] -== Test the application - -. Run tests -+ -[source, shell] ----- -$ pytest ----- - -== Problem Description - -Schedule meetings between employees, where: - -* Each meeting has a topic, duration, required and preferred attendees. -* Each meeting needs a room with sufficient capacity. -* Meetings should not overlap with other meetings if they share resources (room or attendees). -* Meetings should be scheduled as soon as possible. -* Preferred attendees should be able to attend if possible. - -== More information - -Visit https://timefold.ai[timefold.ai]. \ No newline at end of file diff --git a/fast/meeting-scheduling-fast/src/meeting_scheduling/__init__.py b/fast/meeting-scheduling-fast/src/meeting_scheduling/__init__.py deleted file mode 100644 index 0865446..0000000 --- a/fast/meeting-scheduling-fast/src/meeting_scheduling/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import uvicorn -from .rest_api import app - -def main(): - config = uvicorn.Config("meeting_scheduling:app", - port=8080, - log_config="logging.conf", - use_colors=True) - server = uvicorn.Server(config) - server.run() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/fast/meeting-scheduling-fast/src/meeting_scheduling/constraints.py b/fast/meeting-scheduling-fast/src/meeting_scheduling/constraints.py deleted file mode 100644 index 61aecb0..0000000 --- a/fast/meeting-scheduling-fast/src/meeting_scheduling/constraints.py +++ /dev/null @@ -1,351 +0,0 @@ -from timefold.solver.score import (constraint_provider, HardMediumSoftScore, Joiners, - ConstraintFactory, Constraint) - -from .domain import * - -@constraint_provider -def define_constraints(constraint_factory: ConstraintFactory): - """ - Defines all constraints for the meeting scheduling problem, organized by priority (hard, medium, soft). - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - List[Constraint]: All defined constraints. - """ - return [ - # Hard constraints - room_conflict(constraint_factory), - avoid_overtime(constraint_factory), - required_attendance_conflict(constraint_factory), - required_room_capacity(constraint_factory), - start_and_end_on_same_day(constraint_factory), - # Medium constraints - required_and_preferred_attendance_conflict(constraint_factory), - preferred_attendance_conflict(constraint_factory), - # Soft constraints - do_meetings_as_soon_as_possible(constraint_factory), - one_break_between_consecutive_meetings(constraint_factory), - overlapping_meetings(constraint_factory), - assign_larger_rooms_first(constraint_factory), - room_stability(constraint_factory) - ] - -# ************************************************************************ -# Hard constraints -# ************************************************************************ - -def room_conflict(constraint_factory: ConstraintFactory) -> Constraint: - """ - Hard constraint: Prevents overlapping meetings in the same room. - - Penalizes pairs of meetings scheduled in the same room whose time slots overlap, with penalty proportional to the overlap duration. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each_unique_pair(MeetingAssignment, - Joiners.equal(lambda assignment: assignment.room), - Joiners.overlapping(lambda assignment: assignment.get_grain_index(), - lambda assignment: assignment.get_last_time_grain_index() + 1)) - .penalize(HardMediumSoftScore.ONE_HARD, - lambda left_assignment, right_assignment: right_assignment.calculate_overlap(left_assignment)) - .as_constraint("Room conflict")) - - -def avoid_overtime(constraint_factory: ConstraintFactory) -> Constraint: - """ - Hard constraint: Prevents meetings from extending beyond available time slots. - - Penalizes meetings that end after the last available time grain, based on how far they extend beyond the schedule. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each_including_unassigned(MeetingAssignment) - .filter(lambda meeting_assignment: meeting_assignment.starting_time_grain is not None) - .if_not_exists(TimeGrain, - Joiners.equal(lambda assignment: assignment.get_last_time_grain_index(), - lambda time_grain: time_grain.grain_index)) - .penalize(HardMediumSoftScore.ONE_HARD, - lambda meeting_assignment: meeting_assignment.get_last_time_grain_index()) - .as_constraint("Don't go in overtime")) - - -def required_attendance_conflict(constraint_factory: ConstraintFactory) -> Constraint: - """ - Hard constraint: Prevents required attendees from having overlapping meetings. - - Penalizes when a person required at multiple meetings is scheduled for overlapping meetings, proportional to the overlap duration. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each_unique_pair(RequiredAttendance, - Joiners.equal(lambda attendance: attendance.person)) - .join(MeetingAssignment, - Joiners.equal(lambda left_required, right_required: left_required.meeting_id, - lambda assignment: assignment.meeting.id)) - .join(MeetingAssignment, - Joiners.equal(lambda left_required, right_required, left_assignment: right_required.meeting_id, - lambda assignment: assignment.meeting.id), - Joiners.overlapping(lambda attendee1, attendee2, assignment: assignment.get_grain_index(), - lambda attendee1, attendee2, assignment: assignment.get_last_time_grain_index() + 1, - lambda assignment: assignment.get_grain_index(), - lambda assignment: assignment.get_last_time_grain_index() + 1)) - .penalize(HardMediumSoftScore.ONE_HARD, - lambda left_required, right_required, left_assignment, right_assignment: - right_assignment.calculate_overlap(left_assignment)) - .as_constraint("Required attendance conflict")) - - -def required_room_capacity(constraint_factory: ConstraintFactory) -> Constraint: - """ - Hard constraint: Ensures rooms have enough capacity for required attendees. - - Penalizes meetings assigned to rooms with insufficient capacity, proportional to the shortfall. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each_including_unassigned(MeetingAssignment) - .filter(lambda meeting_assignment: meeting_assignment.get_required_capacity() > meeting_assignment.get_room_capacity()) - .penalize(HardMediumSoftScore.ONE_HARD, - lambda meeting_assignment: meeting_assignment.get_required_capacity() - meeting_assignment.get_room_capacity()) - .as_constraint("Required room capacity")) - - -def start_and_end_on_same_day(constraint_factory: ConstraintFactory) -> Constraint: - """ - Hard constraint: Ensures meetings start and end on the same day. - - Penalizes meetings that span multiple days. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each_including_unassigned(MeetingAssignment) - .filter(lambda meeting_assignment: meeting_assignment.starting_time_grain is not None) - .join(TimeGrain, - Joiners.equal(lambda meeting_assignment: meeting_assignment.get_last_time_grain_index(), - lambda time_grain: time_grain.grain_index), - Joiners.filtering(lambda meeting_assignment, time_grain: - meeting_assignment.starting_time_grain.day_of_year != time_grain.day_of_year)) - .penalize(HardMediumSoftScore.ONE_HARD) - .as_constraint("Start and end on same day")) - - -# ************************************************************************ -# Medium constraints -# ************************************************************************ - -def required_and_preferred_attendance_conflict(constraint_factory: ConstraintFactory) -> Constraint: - """ - Medium constraint: Discourages conflicts between required and preferred attendance for the same person. - - Penalizes when a person required at one meeting and preferred at another is scheduled for overlapping meetings, proportional to the overlap duration. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each(RequiredAttendance) - .join(PreferredAttendance, - Joiners.equal(lambda required: required.person, - lambda preferred: preferred.person)) - .join(MeetingAssignment, - Joiners.equal(lambda required, preferred: required.meeting_id, - lambda assignment: assignment.meeting.id)) - .join(MeetingAssignment, - Joiners.equal(lambda required, preferred, left_assignment: preferred.meeting_id, - lambda assignment: assignment.meeting.id), - Joiners.overlapping(lambda required, preferred, assignment: assignment.get_grain_index(), - lambda required, preferred, assignment: assignment.get_last_time_grain_index() + 1, - lambda assignment: assignment.get_grain_index(), - lambda assignment: assignment.get_last_time_grain_index() + 1)) - .penalize(HardMediumSoftScore.ONE_MEDIUM, - lambda required, preferred, left_assignment, right_assignment: - right_assignment.calculate_overlap(left_assignment)) - .as_constraint("Required and preferred attendance conflict")) - - -def preferred_attendance_conflict(constraint_factory: ConstraintFactory) -> Constraint: - """ - Medium constraint: Discourages conflicts between preferred attendees. - - Penalizes when a person preferred at multiple meetings is scheduled for overlapping meetings, proportional to the overlap duration. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each_unique_pair(PreferredAttendance, - Joiners.equal(lambda attendance: attendance.person)) - .join(MeetingAssignment, - Joiners.equal(lambda left_attendance, right_attendance: left_attendance.meeting_id, - lambda assignment: assignment.meeting.id)) - .join(MeetingAssignment, - Joiners.equal(lambda left_attendance, right_attendance, left_assignment: right_attendance.meeting_id, - lambda assignment: assignment.meeting.id), - Joiners.overlapping(lambda attendee1, attendee2, assignment: assignment.get_grain_index(), - lambda attendee1, attendee2, assignment: assignment.get_last_time_grain_index() + 1, - lambda assignment: assignment.get_grain_index(), - lambda assignment: assignment.get_last_time_grain_index() + 1)) - .penalize(HardMediumSoftScore.ONE_MEDIUM, - lambda left_attendance, right_attendance, left_assignment, right_assignment: - right_assignment.calculate_overlap(left_assignment)) - .as_constraint("Preferred attendance conflict")) - - -# ************************************************************************ -# Soft constraints -# ************************************************************************ - -def do_meetings_as_soon_as_possible(constraint_factory: ConstraintFactory) -> Constraint: - """ - Soft constraint: Encourages scheduling meetings earlier in the available time slots. - - Penalizes meetings scheduled later in the available time grains, proportional to their end time. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each_including_unassigned(MeetingAssignment) - .filter(lambda meeting_assignment: meeting_assignment.starting_time_grain is not None) - .penalize(HardMediumSoftScore.ONE_SOFT, - lambda meeting_assignment: meeting_assignment.get_last_time_grain_index()) - .as_constraint("Do all meetings as soon as possible")) - - -def one_break_between_consecutive_meetings(constraint_factory: ConstraintFactory) -> Constraint: - """ - Soft constraint: Penalizes consecutive meetings without a break. - - Penalizes pairs of meetings that are scheduled consecutively without at least one time grain break between them. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each_including_unassigned(MeetingAssignment) - .filter(lambda meeting_assignment: meeting_assignment.starting_time_grain is not None) - .join(constraint_factory.for_each_including_unassigned(MeetingAssignment) - .filter(lambda assignment: assignment.starting_time_grain is not None), - Joiners.equal(lambda left_assignment: left_assignment.get_last_time_grain_index(), - lambda right_assignment: right_assignment.get_grain_index() - 1)) - .penalize(HardMediumSoftScore.of_soft(100)) - .as_constraint("One TimeGrain break between two consecutive meetings")) - - -def overlapping_meetings(constraint_factory: ConstraintFactory) -> Constraint: - """ - Soft constraint: Discourages overlapping meetings, even in different rooms. - - Penalizes pairs of meetings that overlap in time, regardless of room, proportional to the overlap duration. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each_including_unassigned(MeetingAssignment) - .filter(lambda meeting_assignment: meeting_assignment.starting_time_grain is not None) - .join(constraint_factory.for_each_including_unassigned(MeetingAssignment) - .filter(lambda meeting_assignment: meeting_assignment.starting_time_grain is not None), - Joiners.greater_than(lambda left_assignment: left_assignment.meeting.id, - lambda right_assignment: right_assignment.meeting.id), - Joiners.overlapping(lambda assignment: assignment.get_grain_index(), - lambda assignment: assignment.get_last_time_grain_index() + 1)) - .penalize(HardMediumSoftScore.of_soft(10), - lambda left_assignment, right_assignment: left_assignment.calculate_overlap(right_assignment)) - .as_constraint("Overlapping meetings")) - - -def assign_larger_rooms_first(constraint_factory: ConstraintFactory) -> Constraint: - """ - Soft constraint: Penalizes using smaller rooms when larger rooms are available. - - Penalizes when a meeting is assigned to a room while larger rooms exist, proportional to the capacity difference. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - return (constraint_factory - .for_each_including_unassigned(MeetingAssignment) - .filter(lambda meeting_assignment: meeting_assignment.room is not None) - .join(Room, - Joiners.less_than(lambda meeting_assignment: meeting_assignment.get_room_capacity(), - lambda room: room.capacity)) - .penalize(HardMediumSoftScore.ONE_SOFT, - lambda meeting_assignment, room: room.capacity - meeting_assignment.get_room_capacity()) - .as_constraint("Assign larger rooms first")) - - -def room_stability(constraint_factory: ConstraintFactory) -> Constraint: - """ - Soft constraint: Encourages room stability for people attending multiple meetings. - - Penalizes when a person attends meetings in different rooms that are close in time, encouraging room stability. - This handles both required and preferred attendees. - - Args: - constraint_factory (ConstraintFactory): The constraint factory. - Returns: - Constraint: The defined constraint. - """ - def create_attendance_stability_stream(attendance_type): - return (constraint_factory - .for_each(attendance_type) - .join(attendance_type, - Joiners.equal(lambda left_attendance: left_attendance.person, - lambda right_attendance: right_attendance.person), - Joiners.filtering(lambda left_attendance, right_attendance: - left_attendance.meeting_id != right_attendance.meeting_id)) - .join(MeetingAssignment, - Joiners.equal(lambda left_attendance, right_attendance: left_attendance.meeting_id, - lambda assignment: assignment.meeting.id)) - .join(MeetingAssignment, - Joiners.equal(lambda left_attendance, right_attendance, left_assignment: right_attendance.meeting_id, - lambda assignment: assignment.meeting.id), - Joiners.less_than(lambda left_attendance, right_attendance, left_assignment: left_assignment.get_grain_index(), - lambda assignment: assignment.get_grain_index()), - Joiners.filtering(lambda left_attendance, right_attendance, left_assignment, right_assignment: - left_assignment.room != right_assignment.room), - Joiners.filtering(lambda left_attendance, right_attendance, left_assignment, right_assignment: - right_assignment.get_grain_index() - - left_assignment.meeting.duration_in_grains - - left_assignment.get_grain_index() <= 2)) - .penalize(HardMediumSoftScore.ONE_SOFT)) - - # Combine both required and preferred attendance stability - # Note: Since Python Timefold doesn't have constraint combining like Java, - # we'll use the required attendance version as the primary one - # TODO: In a full implementation, both streams would need to be properly combined - return create_attendance_stability_stream(RequiredAttendance).as_constraint("Room stability") \ No newline at end of file diff --git a/fast/meeting-scheduling-fast/src/meeting_scheduling/demo_data.py b/fast/meeting-scheduling-fast/src/meeting_scheduling/demo_data.py deleted file mode 100644 index 99f21d2..0000000 --- a/fast/meeting-scheduling-fast/src/meeting_scheduling/demo_data.py +++ /dev/null @@ -1,225 +0,0 @@ -from random import Random -from datetime import datetime, timedelta -from enum import Enum -from typing import List, Tuple, Any, Callable -from dataclasses import dataclass - -from .domain import Person, TimeGrain, Room, Meeting, MeetingAssignment, MeetingSchedule, RequiredAttendance, PreferredAttendance - -class DemoData(str, Enum): - SMALL = "SMALL" - MEDIUM = "MEDIUM" - LARGE = "LARGE" - -@dataclass(frozen=True, kw_only=True) -class CountDistribution: - count: int - weight: float - -def counts(distributions: tuple[CountDistribution, ...]) -> tuple[int, ...]: - return tuple(distribution.count for distribution in distributions) - -def weights(distributions: tuple[CountDistribution, ...]) -> tuple[float, ...]: - return tuple(distribution.weight for distribution in distributions) - -def generate_demo_data() -> MeetingSchedule: - """Generate demo data for the meeting scheduling problem.""" - rnd = Random(0) # For reproducible results - - # People - people = generate_people(20, rnd) - - # Time grains - time_grains = generate_time_grains() - - # Rooms - rooms = [ - Room(id="R1", name="Room 1", capacity=30), - Room(id="R2", name="Room 2", capacity=20), - Room(id="R3", name="Room 3", capacity=16) - ] - - # Meetings - meetings = generate_meetings(people, rnd) - - # Rebuild meetings with correct attendances - all_required_attendances = [ra for meeting in meetings for ra in meeting.required_attendances] - all_preferred_attendances = [pa for meeting in meetings for pa in meeting.preferred_attendances] - new_meetings = [] - for m in meetings: - new_meetings.append( - type(m)( - id=m.id, - topic=m.topic, - duration_in_grains=m.duration_in_grains, - speakers=m.speakers, - content=m.content or "", - entire_group_meeting=m.entire_group_meeting, - required_attendances=[a for a in all_required_attendances if a.meeting_id == m.id], - preferred_attendances=[a for a in all_preferred_attendances if a.meeting_id == m.id], - ) - ) - meetings = new_meetings - - # Meeting assignments - meeting_assignments = generate_meeting_assignments(meetings) - - # Create schedule - schedule = MeetingSchedule( - people=people, - time_grains=time_grains, - rooms=rooms, - meetings=meetings, - meeting_assignments=meeting_assignments, - required_attendances=[ra for meeting in meetings for ra in meeting.required_attendances], - preferred_attendances=[pa for meeting in meetings for pa in meeting.preferred_attendances], - ) - - return schedule - - -def generate_people(count_people: int, rnd: Random) -> List[Person]: - """Generate a list of people.""" - FIRST_NAMES = ["Amy", "Beth", "Carl", "Dan", "Elsa", "Flo", "Gus", "Hugo", "Ivy", "Jay", - "Jeri", "Hope", "Avis", "Lino", "Lyle", "Nick", "Dino", "Otha", "Gwen", "Jose", - "Dena", "Jana", "Dave", "Russ", "Josh", "Dana", "Katy"] - LAST_NAMES = ["Cole", "Fox", "Green", "Jones", "King", "Li", "Poe", "Rye", "Smith", "Watt", - "Howe", "Lowe", "Wise", "Clay", "Carr", "Hood", "Long", "Horn", "Haas", "Meza"] - - def generate_name() -> str: - first_name = rnd.choice(FIRST_NAMES) - last_name = rnd.choice(LAST_NAMES) - return f"{first_name} {last_name}" - - return [Person(id=str(i), full_name=generate_name()) for i in range(count_people)] - - -def generate_time_grains() -> List[TimeGrain]: - """Generate time grains for the next 4 days starting from tomorrow.""" - time_grains = [] - current_date = datetime.now().date() + timedelta(days=1) - count = 0 - - while current_date < datetime.now().date() + timedelta(days=5): # Match Java: from +1 to +5 (4 days) - current_time = datetime.combine(current_date, datetime.min.time()) + timedelta(hours=8) # Start at 8:00 - end_time = datetime.combine(current_date, datetime.min.time()) + timedelta(hours=17, minutes=45) # End at 17:45 - - while current_time <= end_time: - day_of_year = current_date.timetuple().tm_yday - minutes_of_day = current_time.hour * 60 + current_time.minute - - count += 1 # Pre-increment like Java ++count - time_grains.append(TimeGrain( - id=str(count), - grain_index=count, - day_of_year=day_of_year, - starting_minute_of_day=minutes_of_day - )) - current_time += timedelta(minutes=15) # 15-minute increments - - current_date += timedelta(days=1) - - return time_grains - - -def generate_meetings(people: List[Person], rnd: Random) -> List[Meeting]: - """Generate meetings with topics and attendees.""" - meeting_topics = [ - "Strategize B2B", "Fast track e-business", "Cross sell virtualization", - "Profitize multitasking", "Transform one stop shop", "Engage braindumps", - "Downsize data mining", "Ramp up policies", "On board synergies", - "Reinvigorate user experience", "Strategize e-business", "Fast track virtualization", - "Cross sell multitasking", "Profitize one stop shop", "Transform braindumps", - "Engage data mining", "Downsize policies", "Ramp up synergies", - "On board user experience", "Reinvigorate B2B", "Strategize virtualization", - "Fast track multitasking", "Cross sell one stop shop", "Reinvigorate multitasking" - ] - - meetings = [] - for i, topic in enumerate(meeting_topics): - meeting = Meeting(id=str(i), topic=topic, duration_in_grains=0) - meetings.append(meeting) - - # Set durations using CountDistribution and random.choices - duration_distribution = ( - CountDistribution(count=8, weight=1), # 33% with 8 time grains - CountDistribution(count=12, weight=1), # 33% with 12 time grains - CountDistribution(count=16, weight=1) # 33% with 16 time grains - ) - - for meeting in meetings: - duration_time_grains, = rnd.choices(population=counts(duration_distribution), - weights=weights(duration_distribution)) - meeting.duration_in_grains = duration_time_grains - - # Add required attendees using CountDistribution - slightly reduced to make more feasible - required_attendees_distribution = ( - CountDistribution(count=2, weight=0.45), # More 2-person meetings - CountDistribution(count=3, weight=0.15), # More 3-person meetings - CountDistribution(count=4, weight=0.10), # Increased 4-person - CountDistribution(count=5, weight=0.10), # Slightly more 5-person - CountDistribution(count=6, weight=0.08), # Reduced larger meetings - CountDistribution(count=7, weight=0.05), - CountDistribution(count=8, weight=0.04), - CountDistribution(count=10, weight=0.03) # Reduced 10-person meetings - ) - - def add_required_attendees(meeting: Meeting, count: int) -> None: - # Use random.sample to avoid duplicates - selected_people = rnd.sample(people, count) - for person in selected_people: - meeting.required_attendances.append( - RequiredAttendance( - id=f"{meeting.id}-{len(meeting.required_attendances) + 1}", - person=person, - meeting_id=meeting.id - ) - ) - - for meeting in meetings: - count, = rnd.choices(population=counts(required_attendees_distribution), - weights=weights(required_attendees_distribution)) - add_required_attendees(meeting, count) - - # Add preferred attendees using CountDistribution - reduced to make more feasible - preferred_attendees_distribution = ( - CountDistribution(count=1, weight=0.25), # More 1-person preferred - CountDistribution(count=2, weight=0.30), # More 2-person preferred - CountDistribution(count=3, weight=0.20), # More 3-person preferred - CountDistribution(count=4, weight=0.10), # Increased 4-person - CountDistribution(count=5, weight=0.06), # Slightly more 5-person - CountDistribution(count=6, weight=0.04), # Reduced larger groups - CountDistribution(count=7, weight=0.02), # Reduced - CountDistribution(count=8, weight=0.02), # Reduced - CountDistribution(count=9, weight=0.01), # Minimal large groups - CountDistribution(count=10, weight=0.00) # Eliminated 10-person preferred - ) - - def add_preferred_attendees(meeting: Meeting, count: int) -> None: - # Get people not already required for this meeting - required_people_ids = {ra.person.id for ra in meeting.required_attendances} - available_people = [person for person in people if person.id not in required_people_ids] - - # Use random.sample to avoid duplicates, but only if we have enough people - if len(available_people) >= count: - selected_people = rnd.sample(available_people, count) - for person in selected_people: - meeting.preferred_attendances.append( - PreferredAttendance( - id=f"{meeting.id}-{len(meeting.required_attendances) + len(meeting.preferred_attendances) + 1}", - person=person, - meeting_id=meeting.id - ) - ) - - for meeting in meetings: - count, = rnd.choices(population=counts(preferred_attendees_distribution), - weights=weights(preferred_attendees_distribution)) - add_preferred_attendees(meeting, count) - - return meetings - - -def generate_meeting_assignments(meetings: List[Meeting]) -> List[MeetingAssignment]: - """Generate meeting assignments for each meeting.""" - return [MeetingAssignment(id=str(i), meeting=meeting) for i, meeting in enumerate(meetings)] diff --git a/fast/meeting-scheduling-fast/src/meeting_scheduling/solver.py b/fast/meeting-scheduling-fast/src/meeting_scheduling/solver.py deleted file mode 100644 index a25cbc3..0000000 --- a/fast/meeting-scheduling-fast/src/meeting_scheduling/solver.py +++ /dev/null @@ -1,20 +0,0 @@ -from timefold.solver import SolverManager, SolverFactory, SolutionManager -from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, - TerminationConfig, Duration) - -from .domain import MeetingSchedule, MeetingAssignment -from .constraints import define_constraints - -solver_config = SolverConfig( - solution_class=MeetingSchedule, - entity_class_list=[MeetingAssignment], - score_director_factory_config=ScoreDirectorFactoryConfig( - constraint_provider_function=define_constraints - ), - termination_config=TerminationConfig( - spent_limit=Duration(seconds=30) - ) -) - -solver_manager = SolverManager.create(SolverFactory.create(solver_config)) -solution_manager = SolutionManager.create(solver_manager) \ No newline at end of file diff --git a/fast/meeting-scheduling-fast/static/app.js b/fast/meeting-scheduling-fast/static/app.js deleted file mode 100644 index 1afa9d6..0000000 --- a/fast/meeting-scheduling-fast/static/app.js +++ /dev/null @@ -1,453 +0,0 @@ -let autoRefreshIntervalId = null; -const formatter = JSJoda.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); -const startTime = formatter.format(JSJoda.LocalDateTime.now().withHour(20).withMinute(0).withSecond(0)); -const endTime = formatter.format(JSJoda.LocalDateTime.now().plusDays(1).withHour(8).withMinute(0).withSecond(0)); -const zoomMin = 1000 * 60 * 60 // one hour in milliseconds -const zoomMax = 4 * 1000 * 60 * 60 * 24 // 5 days in milliseconds - -const byTimelineOptions = { - timeAxis: {scale: "hour", step: 1}, - orientation: {axis: "top"}, - stack: false, - xss: {disabled: true}, // Items are XSS safe through JQuery - zoomMin: zoomMin, - zoomMax: zoomMax, - showCurrentTime: false, - hiddenDates: [ - { - start: startTime, - end: endTime, - repeat: 'daily' - } - ], -}; - -const byRoomPanel = document.getElementById("byRoomPanel"); -let byRoomGroupData = new vis.DataSet(); -let byRoomItemData = new vis.DataSet(); -let byRoomTimeline = new vis.Timeline(byRoomPanel, byRoomItemData, byRoomGroupData, byTimelineOptions); - -const byPersonPanel = document.getElementById("byPersonPanel"); -let byPersonGroupData = new vis.DataSet(); -let byPersonItemData = new vis.DataSet(); -let byPersonTimeline = new vis.Timeline(byPersonPanel, byPersonItemData, byPersonGroupData, byTimelineOptions); - -let scheduleId = null; -let loadedSchedule = null; -let viewType = "R"; - - -$(document).ready(function () { - replaceQuickstartTimefoldAutoHeaderFooter(); - - $("#solveButton").click(function () { - solve(); - }); - $("#stopSolvingButton").click(function () { - stopSolving(); - }); - $("#analyzeButton").click(function () { - analyze(); - }); - $("#byRoomTab").click(function () { - viewType = "R"; - byRoomTimeline.redraw(); - refreshSchedule(); - }); - $("#byPersonTab").click(function () { - viewType = "P"; - byPersonTimeline.redraw(); - refreshSchedule(); - }); - setupAjax(); - refreshSchedule(); -}); - - -function setupAjax() { - $.ajaxSetup({ - headers: { - 'Content-Type': 'application/json', 'Accept': 'application/json,text/plain', // plain text is required by solve() returning UUID of the solver job - } - }); - - // Extend jQuery to support $.put() and $.delete() - jQuery.each(["put", "delete"], function (i, method) { - jQuery[method] = function (url, data, callback, type) { - if (jQuery.isFunction(data)) { - type = type || callback; - callback = data; - data = undefined; - } - return jQuery.ajax({ - url: url, type: method, dataType: type, data: data, success: callback - }); - }; - }); -} - - -function refreshSchedule() { - let path; - if (scheduleId === null) { - path = "/demo-data"; - } else { - path = "/schedules/" + scheduleId; - } - - $.getJSON(path, function (schedule) { - loadedSchedule = schedule; - $('#exportData').attr('href', 'data:text/plain;charset=utf-8,' + JSON.stringify(loadedSchedule)); - renderSchedule(schedule); - }) - .fail(function (xhr, ajaxOptions, thrownError) { - showError("Getting the schedule has failed.", xhr); - refreshSolvingButtons(false); - }); -} - - -function renderSchedule(schedule) { - console.log('Rendering schedule:', schedule); - console.log('Meeting assignments:', schedule.meetingAssignments); - console.log('Meetings:', schedule.meetings); - - refreshSolvingButtons(schedule.solverStatus != null && schedule.solverStatus !== "NOT_SOLVING"); - $("#score").text("Score: " + (schedule.score == null ? "?" : schedule.score)); - - if (viewType === "R") { - renderScheduleByRoom(schedule); - } - if (viewType === "P") { - renderScheduleByPerson(schedule); - } -} - - -function renderScheduleByRoom(schedule) { - const unassigned = $("#unassigned"); - unassigned.children().remove(); - byRoomGroupData.clear(); - byRoomItemData.clear(); - - $.each(schedule.rooms.sort((e1, e2) => e1.name.localeCompare(e2.name)), (_, room) => { - let content = `
${room.name}
`; - byRoomGroupData.add({ - id: room.id, - content: content, - }); - }); - - const meetingMap = new Map(); - schedule.meetings.forEach(m => meetingMap.set(m.id, m)); - const timeGrainMap = new Map(); - schedule.timeGrains.forEach(t => timeGrainMap.set(t.id, t)); - const roomMap = new Map(); - schedule.rooms.forEach(r => roomMap.set(r.id, r)); - $.each(schedule.meetingAssignments, (_, assignment) => { - // Handle both string ID and full object for meeting reference - const meet = typeof assignment.meeting === 'string' ? meetingMap.get(assignment.meeting) : assignment.meeting; - // Handle both string ID and full object for room reference - const room = typeof assignment.room === 'string' ? roomMap.get(assignment.room) : assignment.room; - // Handle both string ID and full object for timeGrain reference - const timeGrain = typeof assignment.startingTimeGrain === 'string' ? timeGrainMap.get(assignment.startingTimeGrain) : assignment.startingTimeGrain; - - // Skip if meeting is not found - if (!meet) { - console.warn(`Meeting not found for assignment ${assignment.id}`); - return; - } - - if (room == null || timeGrain == null) { - const unassignedElement = $(`
`) - .append($(`
`).text(meet.topic)) - .append($(`

`).text(`${(meet.durationInGrains * 15) / 60} hour(s)`)); - - unassigned.append($(`

`).append($(`
`).append(unassignedElement))); - } else { - const byRoomElement = $("
").append($("
").append($(`
`).text(meet.topic))); - const startDate = JSJoda.LocalDate.now().withDayOfYear(timeGrain.dayOfYear); - const startTime = JSJoda.LocalTime.of(0, 0, 0, 0) - .plusMinutes(timeGrain.startingMinuteOfDay); - const startDateTime = JSJoda.LocalDateTime.of(startDate, startTime); - const endDateTime = startTime.plusMinutes(meet.durationInGrains * 15); - byRoomItemData.add({ - id: assignment.id, - group: typeof room === 'string' ? room : room.id, - content: byRoomElement.html(), - start: startDateTime.toString(), - end: endDateTime.toString(), - style: "min-height: 50px" - }); - } - }); - - byRoomTimeline.setWindow(JSJoda.LocalDateTime.now().plusDays(1).withHour(8).toString(), - JSJoda.LocalDateTime.now().plusDays(1).withHour(17).withMinute(45).toString()); -} - - -function renderScheduleByPerson(schedule) { - const unassigned = $("#unassigned"); - unassigned.children().remove(); - byPersonGroupData.clear(); - byPersonItemData.clear(); - - $.each(schedule.people.sort((e1, e2) => e1.fullName.localeCompare(e2.fullName)), (_, person) => { - let content = `
${person.fullName}
`; - byPersonGroupData.add({ - id: person.id, - content: content, - }); - }); - const meetingMap = new Map(); - schedule.meetings.forEach(m => meetingMap.set(m.id, m)); - const timeGrainMap = new Map(); - schedule.timeGrains.forEach(t => timeGrainMap.set(t.id, t)); - const roomMap = new Map(); - schedule.rooms.forEach(r => roomMap.set(r.id, r)); - $.each(schedule.meetingAssignments, (_, assignment) => { - // Handle both string ID and full object for meeting reference - const meet = typeof assignment.meeting === 'string' ? meetingMap.get(assignment.meeting) : assignment.meeting; - // Handle both string ID and full object for room reference - const room = typeof assignment.room === 'string' ? roomMap.get(assignment.room) : assignment.room; - // Handle both string ID and full object for timeGrain reference - const timeGrain = typeof assignment.startingTimeGrain === 'string' ? timeGrainMap.get(assignment.startingTimeGrain) : assignment.startingTimeGrain; - - // Skip if meeting is not found - if (!meet) { - console.warn(`Meeting not found for assignment ${assignment.id}`); - return; - } - - if (room == null || timeGrain == null) { - const unassignedElement = $(`
`) - .append($(`
`).text(meet.topic)) - .append($(`

`).text(`${(meet.durationInGrains * 15) / 60} hour(s)`)); - - unassigned.append($(`

`).append($(`
`).append(unassignedElement))); - } else { - const startDate = JSJoda.LocalDate.now().withDayOfYear(timeGrain.dayOfYear); - const startTime = JSJoda.LocalTime.of(0, 0, 0, 0) - .plusMinutes(timeGrain.startingMinuteOfDay); - const startDateTime = JSJoda.LocalDateTime.of(startDate, startTime); - const endDateTime = startTime.plusMinutes(meet.durationInGrains * 15); - meet.requiredAttendances.forEach(attendance => { - const byPersonElement = $("
").append($("
").append($(`
`).text(meet.topic))); - byPersonElement.append($("
").append($(``).text("Required"))); - if (meet.preferredAttendances.map(a => a.person).indexOf(attendance.person) >= 0) { - byPersonElement.append($("
").append($(``).text("Preferred"))); - } - byPersonItemData.add({ - id: `${assignment.id}-${attendance.person.id}`, - group: attendance.person.id, - content: byPersonElement.html(), - start: startDateTime.toString(), - end: endDateTime.toString(), - style: "min-height: 50px" - }); - }); - meet.preferredAttendances.forEach(attendance => { - if (meet.requiredAttendances.map(a => a.person).indexOf(attendance.person) === -1) { - const byPersonElement = $("
").append($("
").append($(`
`).text(meet.topic))); - byPersonElement.append($("
").append($(``).text("Preferred"))); - byPersonItemData.add({ - id: `${assignment.id}-${attendance.person.id}`, - group: attendance.person.id, - content: byPersonElement.html(), - start: startDateTime.toString(), - end: endDateTime.toString(), - style: "min-height: 50px" - }); - } - }); - } - }); - - byPersonTimeline.setWindow(JSJoda.LocalDateTime.now().plusDays(1).withHour(8).toString(), - JSJoda.LocalDateTime.now().plusDays(1).withHour(17).withMinute(45).toString()); -} - - -function solve() { - $.post("/schedules", JSON.stringify(loadedSchedule), function (data) { - scheduleId = data; - refreshSolvingButtons(true); - }).fail(function (xhr, ajaxOptions, thrownError) { - showError("Start solving failed.", xhr); - refreshSolvingButtons(false); - }, "text"); -} - - -function analyze() { - new bootstrap.Modal("#scoreAnalysisModal").show() - const scoreAnalysisModalContent = $("#scoreAnalysisModalContent"); - scoreAnalysisModalContent.children().remove(); - if (loadedSchedule.score == null) { - scoreAnalysisModalContent.text("No score to analyze yet, please first press the 'solve' button."); - } else { - $('#scoreAnalysisScoreLabel').text(`(${loadedSchedule.score})`); - $.put("/schedules/analyze", JSON.stringify(loadedSchedule), function (scoreAnalysis) { - let constraints = scoreAnalysis.constraints; - constraints.sort((a, b) => { - let aComponents = getScoreComponents(a.score), bComponents = getScoreComponents(b.score); - if (aComponents.hard < 0 && bComponents.hard > 0) return -1; - if (aComponents.hard > 0 && bComponents.soft < 0) return 1; - if (Math.abs(aComponents.hard) > Math.abs(bComponents.hard)) { - return -1; - } else { - if (aComponents.medium < 0 && bComponents.medium > 0) return -1; - if (aComponents.medium > 0 && bComponents.medium < 0) return 1; - if (Math.abs(aComponents.medium) > Math.abs(bComponents.medium)) { - return -1; - } else { - if (aComponents.soft < 0 && bComponents.soft > 0) return -1; - if (aComponents.soft > 0 && bComponents.soft < 0) return 1; - - return Math.abs(bComponents.soft) - Math.abs(aComponents.soft); - } - } - }); - constraints.map((e) => { - let components = getScoreComponents(e.weight); - e.type = components.hard != 0 ? 'hard' : (components.medium != 0 ? 'medium' : 'soft'); - e.weight = components[e.type]; - let scores = getScoreComponents(e.score); - e.implicitScore = scores.hard != 0 ? scores.hard : (scores.medium != 0 ? scores.medium : scores.soft); - }); - scoreAnalysis.constraints = constraints; - - scoreAnalysisModalContent.children().remove(); - scoreAnalysisModalContent.text(""); - - const analysisTable = $(``).css({textAlign: 'center'}); - const analysisTHead = $(``).append($(``) - .append($(``)) - .append($(``).css({textAlign: 'left'})) - .append($(``)) - .append($(``)) - .append($(``)) - .append($(``)) - .append($(``))); - analysisTable.append(analysisTHead); - const analysisTBody = $(``) - $.each(scoreAnalysis.constraints, (index, constraintAnalysis) => { - let icon = constraintAnalysis.type == "hard" && constraintAnalysis.implicitScore < 0 ? '' : ''; - if (!icon) icon = constraintAnalysis.matches.length == 0 ? '' : ''; - - let row = $(``); - row.append($(` - - - - - `); - }); - // Visits - solution.visits.forEach(function (visit) { - getVisitMarker(visit).setPopupContent(visitPopupContent(visit)); - }); - // Route - routeGroup.clearLayers(); - const visitByIdMap = new Map(solution.visits.map(visit => [visit.id, visit])); - for (let vehicle of solution.vehicles) { - const homeLocation = vehicle.homeLocation; - const locations = vehicle.visits.map(visitId => visitByIdMap.get(visitId).location); - L.polyline([homeLocation, ...locations, homeLocation], {color: colorByVehicle(vehicle)}).addTo(routeGroup); - } - - // Summary - $('#score').text(solution.score); - $('#drivingTime').text(formatDrivingTime(solution.totalDrivingTimeSeconds)); -} - -function renderTimelines(routePlan) { - byVehicleGroupData.clear(); - byVisitGroupData.clear(); - byVehicleItemData.clear(); - byVisitItemData.clear(); - - $.each(routePlan.vehicles, function (index, vehicle) { - const {totalDemand, capacity} = vehicle - const percentage = totalDemand / capacity * 100; - const vehicleWithLoad = `
vehicle-${vehicle.id}
-
-
- ${totalDemand}/${capacity} -
-
` - byVehicleGroupData.add({id: vehicle.id, content: vehicleWithLoad}); - }); - - $.each(routePlan.visits, function (index, visit) { - const minStartTime = JSJoda.LocalDateTime.parse(visit.minStartTime); - const maxEndTime = JSJoda.LocalDateTime.parse(visit.maxEndTime); - const serviceDuration = JSJoda.Duration.ofSeconds(visit.serviceDuration); - - const visitGroupElement = $(`
`) - .append($(`
`).text(`${visit.name}`)); - byVisitGroupData.add({ - id: visit.id, - content: visitGroupElement.html() - }); - - // Time window per visit. - byVisitItemData.add({ - id: visit.id + "_readyToDue", - group: visit.id, - start: visit.minStartTime, - end: visit.maxEndTime, - type: "background", - style: "background-color: #8AE23433" - }); - - if (visit.vehicle == null) { - const byJobJobElement = $(`
`) - .append($(`
`).text(`Unassigned`)); - - // Unassigned are shown at the beginning of the visit's time window; the length is the service duration. - byVisitItemData.add({ - id: visit.id + '_unassigned', - group: visit.id, - content: byJobJobElement.html(), - start: minStartTime.toString(), - end: minStartTime.plus(serviceDuration).toString(), - style: "background-color: #EF292999" - }); - } else { - const arrivalTime = JSJoda.LocalDateTime.parse(visit.arrivalTime); - const beforeReady = arrivalTime.isBefore(minStartTime); - const arrivalPlusService = arrivalTime.plus(serviceDuration); - const afterDue = arrivalPlusService.isAfter(maxEndTime); - - const byVehicleElement = $(`
`) - .append('
') - .append($(`
`).text(visit.name)); - - const byVisitElement = $(`
`) - // visit.vehicle is the vehicle.id due to Jackson serialization - .append($(`
`).text('vehicle-' + visit.vehicle)); - - const byVehicleTravelElement = $(`
`) - .append($(`
`).text('Travel')); - - const previousDeparture = arrivalTime.minusSeconds(visit.drivingTimeSecondsFromPreviousStandstill); - byVehicleItemData.add({ - id: visit.id + '_travel', - group: visit.vehicle, // visit.vehicle is the vehicle.id due to Jackson serialization - subgroup: visit.vehicle, - content: byVehicleTravelElement.html(), - start: previousDeparture.toString(), - end: visit.arrivalTime, - style: "background-color: #f7dd8f90" - }); - if (beforeReady) { - const byVehicleWaitElement = $(`
`) - .append($(`
`).text('Wait')); - - byVehicleItemData.add({ - id: visit.id + '_wait', - group: visit.vehicle, // visit.vehicle is the vehicle.id due to Jackson serialization - subgroup: visit.vehicle, - content: byVehicleWaitElement.html(), - start: visit.arrivalTime, - end: visit.minStartTime - }); - } - let serviceElementBackground = afterDue ? '#EF292999' : '#83C15955' - - byVehicleItemData.add({ - id: visit.id + '_service', - group: visit.vehicle, // visit.vehicle is the vehicle.id due to Jackson serialization - subgroup: visit.vehicle, - content: byVehicleElement.html(), - start: visit.startServiceTime, - end: visit.departureTime, - style: "background-color: " + serviceElementBackground - }); - byVisitItemData.add({ - id: visit.id, - group: visit.id, - content: byVisitElement.html(), - start: visit.startServiceTime, - end: visit.departureTime, - style: "background-color: " + serviceElementBackground - }); - - } - - }); - - $.each(routePlan.vehicles, function (index, vehicle) { - if (vehicle.visits.length > 0) { - let lastVisit = routePlan.visits.filter((visit) => visit.id == vehicle.visits[vehicle.visits.length -1]).pop(); - if (lastVisit) { - byVehicleItemData.add({ - id: vehicle.id + '_travelBackToHomeLocation', - group: vehicle.id, // visit.vehicle is the vehicle.id due to Jackson serialization - subgroup: vehicle.id, - content: $(`
`).append($(`
`).text('Travel')).html(), - start: lastVisit.departureTime, - end: vehicle.arrivalTime, - style: "background-color: #f7dd8f90" - }); - } - } - }); - - if (!initialized) { - if (byVehicleTimeline) { - byVehicleTimeline.setWindow(routePlan.startDateTime, routePlan.endDateTime); - } - if (byVisitTimeline) { - byVisitTimeline.setWindow(routePlan.startDateTime, routePlan.endDateTime); - } - } -} - -function analyze() { - // see score-analysis.js - analyzeScore(loadedRoutePlan, "/route-plans/analyze") -} - -// TODO: move the general functionality to the webjar. - -function setupAjax() { - $.ajaxSetup({ - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json,text/plain', // plain text is required by solve() returning UUID of the solver job - } - }); - - // Extend jQuery to support $.put() and $.delete() - jQuery.each(["put", "delete"], function (i, method) { - jQuery[method] = function (url, data, callback, type) { - if (jQuery.isFunction(data)) { - type = type || callback; - callback = data; - data = undefined; - } - return jQuery.ajax({ - url: url, - type: method, - dataType: type, - data: data, - success: callback - }); - }; - }); -} - -function solve() { - $.post("/route-plans", JSON.stringify(loadedRoutePlan), function (data) { - scheduleId = data; - refreshSolvingButtons(true); - }).fail(function (xhr, ajaxOptions, thrownError) { - showError("Start solving failed.", xhr); - refreshSolvingButtons(false); - }, - "text"); -} - -function refreshSolvingButtons(solving) { - optimizing = solving; - if (solving) { - $("#solveButton").hide(); - $("#visitButton").hide(); - $("#stopSolvingButton").show(); - if (autoRefreshIntervalId == null) { - autoRefreshIntervalId = setInterval(refreshRoutePlan, 2000); - } - } else { - $("#solveButton").show(); - $("#visitButton").show(); - $("#stopSolvingButton").hide(); - if (autoRefreshIntervalId != null) { - clearInterval(autoRefreshIntervalId); - autoRefreshIntervalId = null; - } - } -} - -function refreshRoutePlan() { - let path = "/route-plans/" + scheduleId; - if (scheduleId === null) { - if (demoDataId === null) { - alert("Please select a test data set."); - return; - } - - path = "/demo-data/" + demoDataId; - } - - $.getJSON(path, function (routePlan) { - loadedRoutePlan = routePlan; - refreshSolvingButtons(routePlan.solverStatus != null && routePlan.solverStatus !== "NOT_SOLVING"); - renderRoutes(routePlan); - renderTimelines(routePlan); - initialized = true; - }).fail(function (xhr, ajaxOptions, thrownError) { - showError("Getting route plan has failed.", xhr); - refreshSolvingButtons(false); - }); -} - -function stopSolving() { - $.delete("/route-plans/" + scheduleId, function () { - refreshSolvingButtons(false); - refreshRoutePlan(); - }).fail(function (xhr, ajaxOptions, thrownError) { - showError("Stop solving failed.", xhr); - }); -} - -function fetchDemoData() { - $.get("/demo-data", function (data) { - data.forEach(function (item) { - $("#testDataButton").append($('' + item + '')); - - $("#" + item + "TestData").click(function () { - switchDataDropDownItemActive(item); - scheduleId = null; - demoDataId = item; - initialized = false; - homeLocationGroup.clearLayers(); - homeLocationMarkerByIdMap.clear(); - visitGroup.clearLayers(); - visitMarkerByIdMap.clear(); - refreshRoutePlan(); - }); - }); - - demoDataId = data[0]; - switchDataDropDownItemActive(demoDataId); - - refreshRoutePlan(); - }).fail(function (xhr, ajaxOptions, thrownError) { - // disable this page as there is no data - $("#demo").empty(); - $("#demo").html("

No test data available

") - }); -} - -function switchDataDropDownItemActive(newItem) { - activeCssClass = "active"; - $("#testDataButton > a." + activeCssClass).removeClass(activeCssClass); - $("#" + newItem + "TestData").addClass(activeCssClass); -} - -function copyTextToClipboard(id) { - var text = $("#" + id).text().trim(); - - var dummy = document.createElement("textarea"); - document.body.appendChild(dummy); - dummy.value = text; - dummy.select(); - document.execCommand("copy"); - document.body.removeChild(dummy); -} - -function replaceQuickstartTimefoldAutoHeaderFooter() { - const timefoldHeader = $("header#timefold-auto-header"); - if (timefoldHeader != null) { - timefoldHeader.addClass("bg-black") - timefoldHeader.append( - $(`
- -
`)); - } - - const timefoldFooter = $("footer#timefold-auto-footer"); - if (timefoldFooter != null) { - timefoldFooter.append( - $(``)); - } -} \ No newline at end of file diff --git a/fast/vehicle-routing-fast/static/index.html b/fast/vehicle-routing-fast/static/index.html deleted file mode 100644 index b8799cf..0000000 --- a/fast/vehicle-routing-fast/static/index.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - - Vehicle Routing - Timefold Solver for Python - - - - - - - - - -
- -
-
-
-
-
-
-

Vehicle routing with capacity and time windows

-

Generate optimal route plan of a vehicle fleet with limited vehicle capacity and time windows.

-
-
-
- -
-
- - - Score: ? - -
-
-
- -
- -
-
-
-
-
-
-
-
-
- Solution summary -
-
ConstraintType# MatchesWeightScore
`).html(icon)) - .append($(``).text(constraintAnalysis.name).css({textAlign: 'left'})) - .append($(``).text(constraintAnalysis.type)) - .append($(``).html(`${constraintAnalysis.matches.length}`)) - .append($(``).text(constraintAnalysis.weight)) - .append($(``).text(constraintAnalysis.implicitScore)); - analysisTBody.append(row); - row.append($(``)); - }); - analysisTable.append(analysisTBody); - scoreAnalysisModalContent.append(analysisTable); - }).fail(function (xhr, ajaxOptions, thrownError) { - showError("Analyze failed.", xhr); - }, "text"); - } -} - - -function getScoreComponents(score) { - let components = {hard: 0, medium: 0, soft: 0}; - - $.each([...score.matchAll(/(-?[0-9]+)(hard|medium|soft)/g)], (i, parts) => { - components[parts[2]] = parseInt(parts[1], 10); - }); - - return components; -} - - -function refreshSolvingButtons(solving) { - if (solving) { - $("#solveButton").hide(); - $("#stopSolvingButton").show(); - if (autoRefreshIntervalId == null) { - autoRefreshIntervalId = setInterval(refreshSchedule, 2000); - } - } else { - $("#solveButton").show(); - $("#stopSolvingButton").hide(); - if (autoRefreshIntervalId != null) { - clearInterval(autoRefreshIntervalId); - autoRefreshIntervalId = null; - } - } -} - -function stopSolving() { - $.delete("/schedules/" + scheduleId, function () { - refreshSolvingButtons(false); - refreshSchedule(); - }).fail(function (xhr, ajaxOptions, thrownError) { - showError("Stop solving failed.", xhr); - }); -} - - -function copyTextToClipboard(id) { - var text = $("#" + id).text().trim(); - - var dummy = document.createElement("textarea"); - document.body.appendChild(dummy); - dummy.value = text; - dummy.select(); - document.execCommand("copy"); - document.body.removeChild(dummy); -} - - -function replaceQuickstartTimefoldAutoHeaderFooter() { - const timefoldHeader = $("header#timefold-auto-header"); - if (timefoldHeader != null) { - timefoldHeader.addClass("bg-black") - timefoldHeader.append($(`
- -
`)); - } - - const timefoldFooter = $("footer#timefold-auto-footer"); - if (timefoldFooter != null) { - timefoldFooter.append($(``)); - } -} diff --git a/fast/meeting-scheduling-fast/tests/test_constraints.py b/fast/meeting-scheduling-fast/tests/test_constraints.py deleted file mode 100644 index 1774c99..0000000 --- a/fast/meeting-scheduling-fast/tests/test_constraints.py +++ /dev/null @@ -1,202 +0,0 @@ -from timefold.solver.test import ConstraintVerifier - -from meeting_scheduling.domain import * -from meeting_scheduling.constraints import ( - define_constraints, - room_conflict, - avoid_overtime, - required_attendance_conflict, - required_room_capacity, - start_and_end_on_same_day -) - - -DEFAULT_TIME_GRAINS = [ - TimeGrain(id=str(i+1), grain_index=i, day_of_year=1, - starting_minute_of_day=480 + i*15) - for i in range(8) -] - -DEFAULT_ROOM = Room(id="1", name="Room 1", capacity=10) -SMALL_ROOM = Room(id="2", name="Small Room", capacity=1) -LARGE_ROOM = Room(id="3", name="Large Room", capacity=2) - - -constraint_verifier = ConstraintVerifier.build(define_constraints, MeetingSchedule, MeetingAssignment) - - -def test_room_conflict_unpenalized(): - """Test that no penalty is applied when meetings in the same room do not overlap.""" - meeting1 = create_meeting(1) - left_assignment = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM) - - meeting2 = create_meeting(2) - right_assignment = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[4], DEFAULT_ROOM) - - constraint_verifier.verify_that(room_conflict).given(left_assignment, right_assignment).penalizes(0) - - -def test_room_conflict_penalized(): - """Test that a penalty is applied when meetings in the same room overlap.""" - meeting1 = create_meeting(1) - left_assignment = create_meeting_assignment(0, meeting1, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM) - - meeting2 = create_meeting(2) - right_assignment = create_meeting_assignment(1, meeting2, DEFAULT_TIME_GRAINS[2], DEFAULT_ROOM) - - constraint_verifier.verify_that(room_conflict).given(left_assignment, right_assignment).penalizes_by(2) - - -def test_avoid_overtime_unpenalized(): - """Test that no penalty is applied when a meeting fits within available time grains (no overtime).""" - meeting = create_meeting(1) - meeting_assignment = create_meeting_assignment(0, meeting, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM) - - constraint_verifier.verify_that(avoid_overtime).given(meeting_assignment, *DEFAULT_TIME_GRAINS).penalizes(0) - - -def test_avoid_overtime_penalized(): - """Test that a penalty is applied when a meeting exceeds available time grains (overtime).""" - meeting = create_meeting(1) - meeting_assignment = create_meeting_assignment(0, meeting, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM) - - constraint_verifier.verify_that(avoid_overtime).given(meeting_assignment).penalizes_by(3) - - -def test_required_attendance_conflict_unpenalized(): - """Test that no penalty is applied when a person does not have overlapping required meetings.""" - person = create_person(1) - - left_meeting = create_meeting(1, duration=2) - required_attendance1 = create_required_attendance(0, person, left_meeting) - - right_meeting = create_meeting(2, duration=2) - required_attendance2 = create_required_attendance(1, person, right_meeting) - - left_assignment = create_meeting_assignment(0, left_meeting, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM) - right_assignment = create_meeting_assignment(1, right_meeting, DEFAULT_TIME_GRAINS[2], DEFAULT_ROOM) - - constraint_verifier.verify_that(required_attendance_conflict).given( - required_attendance1, required_attendance2, - left_assignment, right_assignment - ).penalizes(0) - - -def test_required_attendance_conflict_penalized(): - """Test that a penalty is applied when a person has overlapping required meetings.""" - person = create_person(1) - - left_meeting = create_meeting(1, duration=2) - required_attendance1 = create_required_attendance(0, person, left_meeting) - - right_meeting = create_meeting(2, duration=2) - required_attendance2 = create_required_attendance(1, person, right_meeting) - - left_assignment = create_meeting_assignment(0, left_meeting, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM) - right_assignment = create_meeting_assignment(1, right_meeting, DEFAULT_TIME_GRAINS[1], DEFAULT_ROOM) - - constraint_verifier.verify_that(required_attendance_conflict).given( - required_attendance1, required_attendance2, - left_assignment, right_assignment - ).penalizes_by(1) - - -def test_required_room_capacity_unpenalized(): - """Test that no penalty is applied when the room has enough capacity for all required and preferred attendees.""" - person1 = create_person(1) - person2 = create_person(2) - - meeting = create_meeting(1, duration=2) - create_required_attendance(0, person1, meeting) - create_preferred_attendance(1, person2, meeting) - - meeting_assignment = create_meeting_assignment(0, meeting, DEFAULT_TIME_GRAINS[0], LARGE_ROOM) - - constraint_verifier.verify_that(required_room_capacity).given(meeting_assignment).penalizes(0) - - -def test_required_room_capacity_penalized(): - """Test that a penalty is applied when the room does not have enough capacity for all required and preferred attendees.""" - person1 = create_person(1) - person2 = create_person(2) - - meeting = create_meeting(1, duration=2) - create_required_attendance(0, person1, meeting) - create_preferred_attendance(1, person2, meeting) - - meeting_assignment = create_meeting_assignment(0, meeting, DEFAULT_TIME_GRAINS[0], SMALL_ROOM) - - constraint_verifier.verify_that(required_room_capacity).given(meeting_assignment).penalizes_by(1) - - -def test_start_and_end_on_same_day_unpenalized(): - """Test that no penalty is applied when a meeting starts and ends on the same day.""" - # Need custom time grains with day_of_year=0 (DEFAULT_TIME_GRAINS use day_of_year=1) - start_time_grain = TimeGrain(id="1", grain_index=0, day_of_year=0, starting_minute_of_day=480) - end_time_grain = TimeGrain(id="2", grain_index=3, day_of_year=0, starting_minute_of_day=525) # Same day - - meeting = create_meeting(1) - meeting_assignment = create_meeting_assignment(0, meeting, start_time_grain, DEFAULT_ROOM) - - constraint_verifier.verify_that(start_and_end_on_same_day).given(meeting_assignment, end_time_grain).penalizes(0) - - -def test_start_and_end_on_same_day_penalized(): - """Test that a penalty is applied when a meeting starts and ends on different days.""" - # Need custom time grains to test different days (start=day 0, end=day 1) - start_time_grain = TimeGrain(id="1", grain_index=0, day_of_year=0, starting_minute_of_day=480) - end_time_grain = TimeGrain(id="2", grain_index=3, day_of_year=1, starting_minute_of_day=525) # Different day - - meeting = create_meeting(1) - meeting_assignment = create_meeting_assignment(0, meeting, start_time_grain, DEFAULT_ROOM) - - constraint_verifier.verify_that(start_and_end_on_same_day).given(meeting_assignment, end_time_grain).penalizes_by(1) - - -def test_multiple_constraint_violations(): - """Test that multiple constraints can be violated simultaneously.""" - person = create_person(1) - - left_meeting = create_meeting(1) - required_attendance1 = create_required_attendance(0, person, left_meeting) - left_assignment = create_meeting_assignment(0, left_meeting, DEFAULT_TIME_GRAINS[0], DEFAULT_ROOM) - - right_meeting = create_meeting(2) - required_attendance2 = create_required_attendance(1, person, right_meeting) - right_assignment = create_meeting_assignment(1, right_meeting, DEFAULT_TIME_GRAINS[2], DEFAULT_ROOM) - - constraint_verifier.verify_that(room_conflict).given(left_assignment, right_assignment).penalizes_by(2) - constraint_verifier.verify_that(required_attendance_conflict).given( - required_attendance1, required_attendance2, left_assignment, right_assignment - ).penalizes_by(2) - - -### Helper functions ### - -def create_meeting(id, topic="Meeting", duration=4): - """Helper to create a meeting with standard parameters.""" - return Meeting(id=str(id), topic=f"{topic} {id}", duration_in_grains=duration) - - -def create_meeting_assignment(id, meeting, time_grain, room): - """Helper to create a meeting assignment.""" - return MeetingAssignment(id=str(id), meeting=meeting, starting_time_grain=time_grain, room=room) - - -def create_person(id): - """Helper to create a person.""" - return Person(id=str(id), full_name=f"Person {id}") - - -def create_required_attendance(id, person, meeting): - """Helper to create and link required attendance.""" - attendance = RequiredAttendance(id=str(id), person=person, meeting_id=meeting.id) - meeting.required_attendances = [attendance] - return attendance - - -def create_preferred_attendance(id, person, meeting): - """Helper to create and link preferred attendance.""" - attendance = PreferredAttendance(id=str(id), person=person, meeting_id=meeting.id) - meeting.preferred_attendances = [attendance] - return attendance \ No newline at end of file diff --git a/fast/vehicle-routing-fast/README.MD b/fast/vehicle-routing-fast/README.MD deleted file mode 100644 index 3be6d14..0000000 --- a/fast/vehicle-routing-fast/README.MD +++ /dev/null @@ -1,64 +0,0 @@ -# Vehicle Routing (Python) - -Find the most efficient routes for a fleet of vehicles. - -![Vehicle Routing Screenshot](./vehicle-routing-screenshot.png) - -- [Prerequisites](#prerequisites) -- [Run the application](#run-the-application) -- [Test the application](#test-the-application) - -> [!TIP] -> [Check out our off-the-shelf model for Field Service Routing](https://app.timefold.ai/models/field-service-routing/v1). This model goes beyond basic Vehicle Routing and supports additional constraints such as priorities, skills, fairness and more. - -## Prerequisites - -1. Install [Python 3.10, 3.11 or 3.12](https://www.python.org/downloads/). - -2. Install JDK 17+, for example with [Sdkman](https://sdkman.io): - ```sh - $ sdk install java - -## Run the application - -1. Git clone the timefold-solver-python repo and navigate to this directory: - ```sh - $ git clone https://github.com/TimefoldAI/timefold-solver-python.git - ... - $ cd timefold-solver-python/quickstarts/vehicle-routing - ``` - -2. Create a virtual environment: - ```sh - $ python -m venv .venv - ``` - -3. Activate the virtual environment: - ```sh - $ . .venv/bin/activate - ``` - -4. Install the application: - ```sh - $ pip install -e . - ``` - -5. Run the application: - ```sh - $ run-app - ``` - -6. Visit [http://localhost:8080](http://localhost:8080) in your browser. - -7. Click on the **Solve** button. - -## Test the application - -1. Run tests: - ```sh - $ pytest - ``` - -## More information - -Visit [timefold.ai](https://timefold.ai). \ No newline at end of file diff --git a/fast/vehicle-routing-fast/src/vehicle_routing/constraints.py b/fast/vehicle-routing-fast/src/vehicle_routing/constraints.py deleted file mode 100644 index 83fb77d..0000000 --- a/fast/vehicle-routing-fast/src/vehicle_routing/constraints.py +++ /dev/null @@ -1,53 +0,0 @@ -from timefold.solver.score import ConstraintFactory, HardSoftScore, constraint_provider - -from .domain import * - -VEHICLE_CAPACITY = "vehicleCapacity" -MINIMIZE_TRAVEL_TIME = "minimizeTravelTime" -SERVICE_FINISHED_AFTER_MAX_END_TIME = "serviceFinishedAfterMaxEndTime" - - -@constraint_provider -def define_constraints(factory: ConstraintFactory): - return [ - # Hard constraints - vehicle_capacity(factory), - service_finished_after_max_end_time(factory), - # Soft constraints - minimize_travel_time(factory) - ] - -############################################## -# Hard constraints -############################################## - - -def vehicle_capacity(factory: ConstraintFactory): - return (factory.for_each(Vehicle) - .filter(lambda vehicle: vehicle.calculate_total_demand() > vehicle.capacity) - .penalize(HardSoftScore.ONE_HARD, - lambda vehicle: vehicle.calculate_total_demand() - vehicle.capacity) - .as_constraint(VEHICLE_CAPACITY) - ) - - -def service_finished_after_max_end_time(factory: ConstraintFactory): - return (factory.for_each(Visit) - .filter(lambda visit: visit.is_service_finished_after_max_end_time()) - .penalize(HardSoftScore.ONE_HARD, - lambda visit: visit.service_finished_delay_in_minutes()) - .as_constraint(SERVICE_FINISHED_AFTER_MAX_END_TIME) - ) - -############################################## -# Soft constraints -############################################## - - -def minimize_travel_time(factory: ConstraintFactory): - return ( - factory.for_each(Vehicle) - .penalize(HardSoftScore.ONE_SOFT, - lambda vehicle: vehicle.calculate_total_driving_time_seconds()) - .as_constraint(MINIMIZE_TRAVEL_TIME) - ) diff --git a/fast/vehicle-routing-fast/src/vehicle_routing/demo_data.py b/fast/vehicle-routing-fast/src/vehicle_routing/demo_data.py deleted file mode 100644 index 8240464..0000000 --- a/fast/vehicle-routing-fast/src/vehicle_routing/demo_data.py +++ /dev/null @@ -1,155 +0,0 @@ -from typing import Generator, TypeVar, Sequence -from datetime import date, datetime, time, timedelta -from enum import Enum -from random import Random -from dataclasses import dataclass - -from .domain import * - - -FIRST_NAMES = ("Amy", "Beth", "Carl", "Dan", "Elsa", "Flo", "Gus", "Hugo", "Ivy", "Jay") -LAST_NAMES = ("Cole", "Fox", "Green", "Jones", "King", "Li", "Poe", "Rye", "Smith", "Watt") -SERVICE_DURATION_MINUTES = (10, 20, 30, 40) -MORNING_WINDOW_START = time(8, 0) -MORNING_WINDOW_END = time(12, 0) -AFTERNOON_WINDOW_START = time(13, 0) -AFTERNOON_WINDOW_END = time(18, 0) - - -@dataclass -class _DemoDataProperties: - seed: int - visit_count: int - vehicle_count: int - vehicle_start_time: time - min_demand: int - max_demand: int - min_vehicle_capacity: int - max_vehicle_capacity: int - south_west_corner: Location - north_east_corner: Location - - def __post_init__(self): - if self.min_demand < 1: - raise ValueError(f"minDemand ({self.min_demand}) must be greater than zero.") - if self.max_demand < 1: - raise ValueError(f"maxDemand ({self.max_demand}) must be greater than zero.") - if self.min_demand >= self.max_demand: - raise ValueError(f"maxDemand ({self.max_demand}) must be greater than minDemand ({self.min_demand}).") - if self.min_vehicle_capacity < 1: - raise ValueError(f"Number of minVehicleCapacity ({self.min_vehicle_capacity}) must be greater than zero.") - if self.max_vehicle_capacity < 1: - raise ValueError(f"Number of maxVehicleCapacity ({self.max_vehicle_capacity}) must be greater than zero.") - if self.min_vehicle_capacity >= self.max_vehicle_capacity: - raise ValueError(f"maxVehicleCapacity ({self.max_vehicle_capacity}) must be greater than " - f"minVehicleCapacity ({self.min_vehicle_capacity}).") - if self.visit_count < 1: - raise ValueError(f"Number of visitCount ({self.visit_count}) must be greater than zero.") - if self.vehicle_count < 1: - raise ValueError(f"Number of vehicleCount ({self.vehicle_count}) must be greater than zero.") - if self.north_east_corner.latitude <= self.south_west_corner.latitude: - raise ValueError(f"northEastCorner.getLatitude ({self.north_east_corner.latitude}) must be greater than " - f"southWestCorner.getLatitude({self.south_west_corner.latitude}).") - if self.north_east_corner.longitude <= self.south_west_corner.longitude: - raise ValueError(f"northEastCorner.getLongitude ({self.north_east_corner.longitude}) must be greater than " - f"southWestCorner.getLongitude({self.south_west_corner.longitude}).") - - -class DemoData(Enum): - PHILADELPHIA = _DemoDataProperties(0, 55, 6, time(7, 30), - 1, 2, 15, 30, - Location(latitude=39.7656099067391, - longitude=-76.83782328143754), - Location(latitude=40.77636644354855, - longitude=-74.9300739430771)) - - HARTFORT = _DemoDataProperties(1, 50, 6, time(7, 30), - 1, 3, 20, 30, - Location(latitude=41.48366520850297, - longitude=-73.15901689943055), - Location(latitude=41.99512052869307, - longitude=-72.25114548877427)) - - FIRENZE = _DemoDataProperties(2, 77, 6, time(7, 30), - 1, 2, 20, 40, - Location(latitude=43.751466, - longitude=11.177210), - Location(latitude=43.809291, - longitude=11.290195)) - - -def doubles(random: Random, start: float, end: float) -> Generator[float, None, None]: - while True: - yield random.uniform(start, end) - - -def ints(random: Random, start: int, end: int) -> Generator[int, None, None]: - while True: - yield random.randrange(start, end) - - -T = TypeVar('T') - - -def values(random: Random, sequence: Sequence[T]) -> Generator[T, None, None]: - start = 0 - end = len(sequence) - 1 - while True: - yield sequence[random.randint(start, end)] - - -def generate_names(random: Random) -> Generator[str, None, None]: - while True: - yield f'{random.choice(FIRST_NAMES)} {random.choice(LAST_NAMES)}' - - -def generate_demo_data(demo_data_enum: DemoData) -> VehicleRoutePlan: - name = "demo" - demo_data = demo_data_enum.value - random = Random(demo_data.seed) - latitudes = doubles(random, demo_data.south_west_corner.latitude, demo_data.north_east_corner.latitude) - longitudes = doubles(random, demo_data.south_west_corner.longitude, demo_data.north_east_corner.longitude) - - demands = ints(random, demo_data.min_demand, demo_data.max_demand + 1) - service_durations = values(random, SERVICE_DURATION_MINUTES) - vehicle_capacities = ints(random, demo_data.min_vehicle_capacity, - demo_data.max_vehicle_capacity + 1) - - vehicles = [Vehicle(id=str(i), - capacity=next(vehicle_capacities), - home_location=Location( - latitude=next(latitudes), - longitude=next(longitudes)), - departure_time=datetime.combine( - date.today() + timedelta(days=1), demo_data.vehicle_start_time) - ) - for i in range(demo_data.vehicle_count)] - - names = generate_names(random) - visits = [ - Visit( - id=str(i), - name=next(names), - location=Location(latitude=next(latitudes), longitude=next(longitudes)), - demand=next(demands), - min_start_time=datetime.combine(date.today() + timedelta(days=1), - MORNING_WINDOW_START - if (morning_window := random.random() > 0.5) - else AFTERNOON_WINDOW_START), - max_end_time=datetime.combine(date.today() + timedelta(days=1), - MORNING_WINDOW_END - if morning_window - else AFTERNOON_WINDOW_END), - service_duration=timedelta(minutes=next(service_durations)), - ) for i in range(demo_data.visit_count) - ] - - return VehicleRoutePlan(name=name, - south_west_corner=demo_data.south_west_corner, - north_east_corner=demo_data.north_east_corner, - vehicles=vehicles, - visits=visits) - - -def tomorrow_at(local_time: time) -> datetime: - return datetime.combine(date.today(), local_time) diff --git a/fast/vehicle-routing-fast/src/vehicle_routing/domain.py b/fast/vehicle-routing-fast/src/vehicle_routing/domain.py deleted file mode 100644 index f0efa5c..0000000 --- a/fast/vehicle-routing-fast/src/vehicle_routing/domain.py +++ /dev/null @@ -1,220 +0,0 @@ -from timefold.solver import SolverStatus -from timefold.solver.score import HardSoftScore -from timefold.solver.domain import * - -from datetime import datetime, timedelta -from typing import Annotated, Optional, List, Union -from dataclasses import dataclass, field -from .json_serialization import JsonDomainBase -from pydantic import Field - - -@dataclass -class Location: - latitude: float - longitude: float - - def driving_time_to(self, other: 'Location') -> int: - return round(( - (self.latitude - other.latitude) ** 2 + - (self.longitude - other.longitude) ** 2 - ) ** 0.5 * 4_000) - - def __str__(self): - return f'[{self.latitude}, {self.longitude}]' - - def __repr__(self): - return f'Location({self.latitude}, {self.longitude})' - - -@planning_entity -@dataclass -class Visit: - id: Annotated[str, PlanningId] - name: str - location: Location - demand: int - min_start_time: datetime - max_end_time: datetime - service_duration: timedelta - vehicle: Annotated[Optional['Vehicle'], - InverseRelationShadowVariable(source_variable_name='visits')] = None - previous_visit: Annotated[Optional['Visit'], - PreviousElementShadowVariable(source_variable_name='visits')] = None - next_visit: Annotated[Optional['Visit'], - NextElementShadowVariable(source_variable_name='visits')] = None - arrival_time: Annotated[ - Optional[datetime], - CascadingUpdateShadowVariable(target_method_name='update_arrival_time')] = None - - def update_arrival_time(self): - if self.vehicle is None or (self.previous_visit is not None and self.previous_visit.arrival_time is None): - self.arrival_time = None - elif self.previous_visit is None: - self.arrival_time = (self.vehicle.departure_time + - timedelta(seconds=self.vehicle.home_location.driving_time_to(self.location))) - else: - self.arrival_time = (self.previous_visit.calculate_departure_time() + - timedelta(seconds=self.previous_visit.location.driving_time_to(self.location))) - - def calculate_departure_time(self): - if self.arrival_time is None: - return None - - return max(self.arrival_time, self.min_start_time) + self.service_duration - - @property - def departure_time(self) -> Optional[datetime]: - return self.calculate_departure_time() - - @property - def start_service_time(self) -> Optional[datetime]: - if self.arrival_time is None: - return None - return max(self.arrival_time, self.min_start_time) - - def is_service_finished_after_max_end_time(self) -> bool: - return self.arrival_time is not None and self.calculate_departure_time() > self.max_end_time - - def service_finished_delay_in_minutes(self) -> int: - if self.arrival_time is None: - return 0 - # Floor division always rounds down, so divide by a negative duration and negate the result - # to round up - # ex: 30 seconds / -1 minute = -0.5, - # so 30 seconds // -1 minute = -1, - # and negating that gives 1 - return -((self.calculate_departure_time() - self.max_end_time) // timedelta(minutes=-1)) - - @property - def driving_time_seconds_from_previous_standstill(self) -> Optional[int]: - if self.vehicle is None: - return None - - if self.previous_visit is None: - return self.vehicle.home_location.driving_time_to(self.location) - else: - return self.previous_visit.location.driving_time_to(self.location) - - def __str__(self): - return self.id - - def __repr__(self): - return f'Visit({self.id})' - - -@planning_entity -@dataclass -class Vehicle: - id: Annotated[str, PlanningId] - capacity: int - home_location: Location - departure_time: datetime - visits: Annotated[list[Visit], - PlanningListVariable] = field(default_factory=list) - - @property - def arrival_time(self) -> datetime: - if len(self.visits) == 0: - return self.departure_time - return (self.visits[-1].departure_time + - timedelta(seconds=self.visits[-1].location.driving_time_to(self.home_location))) - - @property - def total_demand(self) -> int: - return self.calculate_total_demand() - - @property - def total_driving_time_seconds(self) -> int: - return self.calculate_total_driving_time_seconds() - - def calculate_total_demand(self) -> int: - total_demand = 0 - for visit in self.visits: - total_demand += visit.demand - return total_demand - - def calculate_total_driving_time_seconds(self) -> int: - if len(self.visits) == 0: - return 0 - total_driving_time_seconds = 0 - previous_location = self.home_location - - for visit in self.visits: - total_driving_time_seconds += previous_location.driving_time_to(visit.location) - previous_location = visit.location - - total_driving_time_seconds += previous_location.driving_time_to(self.home_location) - return total_driving_time_seconds - - def __str__(self): - return self.id - - def __repr__(self): - return f'Vehicle({self.id})' - - -@planning_solution -@dataclass -class VehicleRoutePlan: - name: str - south_west_corner: Location - north_east_corner: Location - vehicles: Annotated[list[Vehicle], PlanningEntityCollectionProperty] - visits: Annotated[list[Visit], PlanningEntityCollectionProperty, ValueRangeProvider] - score: Annotated[Optional[HardSoftScore], PlanningScore] = None - solver_status: SolverStatus = SolverStatus.NOT_SOLVING - - @property - def total_driving_time_seconds(self) -> int: - out = 0 - for vehicle in self.vehicles: - out += vehicle.total_driving_time_seconds - return out - - def __str__(self): - return f'VehicleRoutePlan(name={self.name}, vehicles={self.vehicles}, visits={self.visits})' - - -# Pydantic REST models for API (used for deserialization and context) -class LocationModel(JsonDomainBase): - latitude: float - longitude: float - - -class VisitModel(JsonDomainBase): - id: str - name: str - location: List[float] # [lat, lng] array - demand: int - min_start_time: str = Field(..., alias="minStartTime") # ISO datetime string - max_end_time: str = Field(..., alias="maxEndTime") # ISO datetime string - service_duration: int = Field(..., alias="serviceDuration") # Duration in seconds - vehicle: Union[str, 'VehicleModel', None] = None - previous_visit: Union[str, 'VisitModel', None] = Field(None, alias="previousVisit") - next_visit: Union[str, 'VisitModel', None] = Field(None, alias="nextVisit") - arrival_time: Optional[str] = Field(None, alias="arrivalTime") # ISO datetime string - departure_time: Optional[str] = Field(None, alias="departureTime") # ISO datetime string - driving_time_seconds_from_previous_standstill: Optional[int] = Field(None, alias="drivingTimeSecondsFromPreviousStandstill") - - -class VehicleModel(JsonDomainBase): - id: str - capacity: int - home_location: List[float] = Field(..., alias="homeLocation") # [lat, lng] array - departure_time: str = Field(..., alias="departureTime") # ISO datetime string - visits: List[Union[str, VisitModel]] = Field(default_factory=list) - total_demand: int = Field(0, alias="totalDemand") - total_driving_time_seconds: int = Field(0, alias="totalDrivingTimeSeconds") - arrival_time: Optional[str] = Field(None, alias="arrivalTime") # ISO datetime string - - -class VehicleRoutePlanModel(JsonDomainBase): - name: str - south_west_corner: List[float] = Field(..., alias="southWestCorner") # [lat, lng] array - north_east_corner: List[float] = Field(..., alias="northEastCorner") # [lat, lng] array - vehicles: List[VehicleModel] - visits: List[VisitModel] - score: Optional[str] = None - solver_status: Optional[str] = None - total_driving_time_seconds: int = Field(0, alias="totalDrivingTimeSeconds") diff --git a/fast/vehicle-routing-fast/src/vehicle_routing/rest_api.py b/fast/vehicle-routing-fast/src/vehicle_routing/rest_api.py deleted file mode 100644 index 5985a15..0000000 --- a/fast/vehicle-routing-fast/src/vehicle_routing/rest_api.py +++ /dev/null @@ -1,76 +0,0 @@ -from fastapi import FastAPI, Depends, Request, HTTPException -from fastapi.staticfiles import StaticFiles -from uuid import uuid4 -from typing import Dict -from dataclasses import asdict - -from .domain import VehicleRoutePlan -from .converters import plan_to_model, model_to_plan -from .domain import VehicleRoutePlanModel, VehicleModel, VisitModel -from .score_analysis import ConstraintAnalysisDTO, MatchAnalysisDTO -from .demo_data import generate_demo_data, DemoData -from .solver import solver_manager, solution_manager - -app = FastAPI(docs_url='/q/swagger-ui') - -data_sets: Dict[str, VehicleRoutePlan] = {} - -@app.get("/demo-data", response_model=VehicleRoutePlanModel) -async def get_demo_data() -> VehicleRoutePlanModel: - """Get a single demo data set (always the same for simplicity).""" - domain_plan = generate_demo_data(DemoData.PHILADELPHIA) - return plan_to_model(domain_plan) - -@app.get("/route-plans/{problem_id}", response_model=VehicleRoutePlanModel, response_model_exclude_none=True) -async def get_route(problem_id: str) -> VehicleRoutePlanModel: - route = data_sets.get(problem_id) - if not route: - raise HTTPException(status_code=404, detail="Route plan not found") - route.solver_status = solver_manager.get_solver_status(problem_id) - return plan_to_model(route) - -@app.post("/route-plans") -async def solve_route(request: Request) -> str: - json_data = await request.json() - job_id = str(uuid4()) - # Parse the incoming JSON using Pydantic models - plan_model = VehicleRoutePlanModel.model_validate(json_data) - # Convert to domain model for solver - domain_plan = model_to_plan(plan_model) - data_sets[job_id] = domain_plan - solver_manager.solve_and_listen( - job_id, - domain_plan, - lambda solution: data_sets.update({job_id: solution}) - ) - return job_id - -@app.put("/route-plans/analyze") -async def analyze_route(request: Request) -> dict: - json_data = await request.json() - plan_model = VehicleRoutePlanModel.model_validate(json_data) - domain_plan = model_to_plan(plan_model) - analysis = solution_manager.analyze(domain_plan) - constraints = [] - for constraint in getattr(analysis, 'constraint_analyses', []) or []: - matches = [ - MatchAnalysisDTO( - name=str(getattr(getattr(match, 'constraint_ref', None), 'constraint_name', "")), - score=str(getattr(match, 'score', "0hard/0soft")), - justification=str(getattr(match, 'justification', "")) - ) - for match in getattr(constraint, 'matches', []) or [] - ] - constraints.append(ConstraintAnalysisDTO( - name=str(getattr(constraint, 'constraint_name', "")), - weight=str(getattr(constraint, 'weight', "0hard/0soft")), - score=str(getattr(constraint, 'score', "0hard/0soft")), - matches=matches - )) - return {"constraints": [asdict(constraint) for constraint in constraints]} - -@app.delete("/route-plans/{problem_id}") -async def stop_solving(problem_id: str) -> None: - solver_manager.terminate_early(problem_id) - -app.mount("/", StaticFiles(directory="static", html=True), name="static") diff --git a/fast/vehicle-routing-fast/static/app.js b/fast/vehicle-routing-fast/static/app.js deleted file mode 100644 index 9716c54..0000000 --- a/fast/vehicle-routing-fast/static/app.js +++ /dev/null @@ -1,546 +0,0 @@ -let autoRefreshIntervalId = null; -let initialized = false; -let optimizing = false; -let demoDataId = null; -let scheduleId = null; -let loadedRoutePlan = null; -let newVisit = null; -let visitMarker = null; -const solveButton = $('#solveButton'); -const stopSolvingButton = $('#stopSolvingButton'); -const vehiclesTable = $('#vehicles'); -const analyzeButton = $('#analyzeButton'); - -/*************************************** Map constants and variable definitions **************************************/ - -const homeLocationMarkerByIdMap = new Map(); -const visitMarkerByIdMap = new Map(); - -const map = L.map('map', {doubleClickZoom: false}).setView([51.505, -0.09], 13); -const visitGroup = L.layerGroup().addTo(map); -const homeLocationGroup = L.layerGroup().addTo(map); -const routeGroup = L.layerGroup().addTo(map); - -/************************************ Time line constants and variable definitions ************************************/ - -let byVehicleTimeline; -let byVisitTimeline; -const byVehicleGroupData = new vis.DataSet(); -const byVehicleItemData = new vis.DataSet(); -const byVisitGroupData = new vis.DataSet(); -const byVisitItemData = new vis.DataSet(); - -const byVehicleTimelineOptions = { - timeAxis: {scale: "hour"}, - orientation: {axis: "top"}, - xss: {disabled: true}, // Items are XSS safe through JQuery - stack: false, - stackSubgroups: false, - zoomMin: 1000 * 60 * 60, // A single hour in milliseconds - zoomMax: 1000 * 60 * 60 * 24 // A single day in milliseconds -}; - -const byVisitTimelineOptions = { - timeAxis: {scale: "hour"}, - orientation: {axis: "top"}, - verticalScroll: true, - xss: {disabled: true}, // Items are XSS safe through JQuery - stack: false, - stackSubgroups: false, - zoomMin: 1000 * 60 * 60, // A single hour in milliseconds - zoomMax: 1000 * 60 * 60 * 24 // A single day in milliseconds -}; - -/************************************ Initialize ************************************/ - -$(document).ready(function () { - replaceQuickstartTimefoldAutoHeaderFooter(); - - // Initialize timelines after DOM is ready with a small delay to ensure Bootstrap tabs are rendered - setTimeout(function() { - const byVehiclePanel = document.getElementById("byVehiclePanel"); - const byVisitPanel = document.getElementById("byVisitPanel"); - - if (byVehiclePanel) { - byVehicleTimeline = new vis.Timeline(byVehiclePanel, byVehicleItemData, byVehicleGroupData, byVehicleTimelineOptions); - } - - if (byVisitPanel) { - byVisitTimeline = new vis.Timeline(byVisitPanel, byVisitItemData, byVisitGroupData, byVisitTimelineOptions); - } - }, 100); - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: '© OpenStreetMap contributors', - }).addTo(map); - - solveButton.click(solve); - stopSolvingButton.click(stopSolving); - analyzeButton.click(analyze); - refreshSolvingButtons(false); - - // HACK to allow vis-timeline to work within Bootstrap tabs - $("#byVehicleTab").on('shown.bs.tab', function (event) { - if (byVehicleTimeline) { - byVehicleTimeline.redraw(); - } - }) - $("#byVisitTab").on('shown.bs.tab', function (event) { - if (byVisitTimeline) { - byVisitTimeline.redraw(); - } - }) - // Add new visit - map.on('click', function (e) { - visitMarker = L.circleMarker(e.latlng); - visitMarker.setStyle({color: 'green'}); - visitMarker.addTo(map); - openRecommendationModal(e.latlng.lat, e.latlng.lng); - }); - // Remove visit mark - $("#newVisitModal").on("hidden.bs.modal", function () { - map.removeLayer(visitMarker); - }); - setupAjax(); - fetchDemoData(); -}); - -function colorByVehicle(vehicle) { - return vehicle === null ? null : pickColor('vehicle' + vehicle.id); -} - -function formatDrivingTime(drivingTimeInSeconds) { - return `${Math.floor(drivingTimeInSeconds / 3600)}h ${Math.round((drivingTimeInSeconds % 3600) / 60)}m`; -} - -function homeLocationPopupContent(vehicle) { - return `
Vehicle ${vehicle.id}
-Home Location`; -} - -function visitPopupContent(visit) { - const arrival = visit.arrivalTime ? `
Arrival at ${showTimeOnly(visit.arrivalTime)}.
` : ''; - return `
${visit.name}
-
Demand: ${visit.demand}
-
Available from ${showTimeOnly(visit.minStartTime)} to ${showTimeOnly(visit.maxEndTime)}.
- ${arrival}`; -} - -function showTimeOnly(localDateTimeString) { - return JSJoda.LocalDateTime.parse(localDateTimeString).toLocalTime(); -} - -function getHomeLocationMarker(vehicle) { - let marker = homeLocationMarkerByIdMap.get(vehicle.id); - if (marker) { - return marker; - } - marker = L.circleMarker(vehicle.homeLocation, { color: colorByVehicle(vehicle), fillOpacity: 0.8 }); - marker.addTo(homeLocationGroup).bindPopup(); - homeLocationMarkerByIdMap.set(vehicle.id, marker); - return marker; -} - -function getVisitMarker(visit) { - let marker = visitMarkerByIdMap.get(visit.id); - if (marker) { - return marker; - } - marker = L.circleMarker(visit.location); - marker.addTo(visitGroup).bindPopup(); - visitMarkerByIdMap.set(visit.id, marker); - return marker; -} - -function renderRoutes(solution) { - if (!initialized) { - const bounds = [solution.southWestCorner, solution.northEastCorner]; - map.fitBounds(bounds); - } - // Vehicles - vehiclesTable.children().remove(); - solution.vehicles.forEach(function (vehicle) { - getHomeLocationMarker(vehicle).setPopupContent(homeLocationPopupContent(vehicle)); - const {id, capacity, totalDemand, totalDrivingTimeSeconds} = vehicle; - const percentage = totalDemand / capacity * 100; - const color = colorByVehicle(vehicle); - vehiclesTable.append(` -
- - - Vehicle ${id} -
-
${totalDemand}/${capacity}
-
-
${formatDrivingTime(totalDrivingTimeSeconds)}
- - - - -
Total driving time:unknown
-
-
-
Vehicles
- - - - - - - - - - -
Name - Load - - Driving time
-
-
-
-
-
- - -
-
-
-
-
-
- -
-

REST API Guide

- -

Vehicle routing with vehicle capacity and time windows - integration via cURL

- -

1. Download demo data

-
-              
-              curl -X GET -H 'Accept:application/json' http://localhost:8080/demo-data/FIRENZE -o sample.json
-    
- -

2. Post the sample data for solving

-

The POST operation returns a jobId that should be used in subsequent commands.

-
-              
-              curl -X POST -H 'Content-Type:application/json' http://localhost:8080/route-plans -d@sample.json
-    
- -

3. Get the current status and score

-
-              
-              curl -X GET -H 'Accept:application/json' http://localhost:8080/route-plans/{jobId}/status
-    
- -

4. Get the complete route plan

-
-              
-              curl -X GET -H 'Accept:application/json' http://localhost:8080/route-plans/{jobId}
-    
- -

5. Terminate solving early

-
-              
-              curl -X DELETE -H 'Accept:application/json' http://localhost:8080/route-plans/{jobId}
-    
-
- -
-

REST API Reference

-
- - -
-
-
- -
- -
-
- - - - - - - - - - - - - - diff --git a/fast/vehicle-routing-fast/test_refactoring.py b/fast/vehicle-routing-fast/test_refactoring.py deleted file mode 100644 index cc8814c..0000000 --- a/fast/vehicle-routing-fast/test_refactoring.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 - -"""Simple test script to verify the refactoring works correctly.""" - -from src.vehicle_routing.domain import Location, Visit, Vehicle, VehicleRoutePlan -from src.vehicle_routing.converters import location_to_model, visit_to_model, vehicle_to_model, plan_to_model -from datetime import datetime, timedelta - -def test_basic_creation(): - """Test that we can create domain objects.""" - print("Testing basic domain object creation...") - - # Create a location - location = Location(latitude=40.0, longitude=-75.0) - print(f"โœ“ Created Location: {location}") - - # Create a visit - visit = Visit( - id="visit1", - name="Test Visit", - location=location, - demand=10, - min_start_time=datetime.now(), - max_end_time=datetime.now() + timedelta(hours=2), - service_duration=timedelta(minutes=30) - ) - print(f"โœ“ Created Visit: {visit}") - - # Create a vehicle - vehicle = Vehicle( - id="vehicle1", - capacity=100, - home_location=location, - departure_time=datetime.now() - ) - print(f"โœ“ Created Vehicle: {vehicle}") - - # Create a plan - plan = VehicleRoutePlan( - name="Test Plan", - south_west_corner=Location(latitude=39.0, longitude=-76.0), - north_east_corner=Location(latitude=41.0, longitude=-74.0), - vehicles=[vehicle], - visits=[visit] - ) - print(f"โœ“ Created VehicleRoutePlan: {plan}") - - return True - -def test_conversion(): - """Test that we can convert between domain and API models.""" - print("\nTesting conversion functions...") - - # Create domain objects - location = Location(latitude=40.0, longitude=-75.0) - visit = Visit( - id="visit1", - name="Test Visit", - location=location, - demand=10, - min_start_time=datetime.now(), - max_end_time=datetime.now() + timedelta(hours=2), - service_duration=timedelta(minutes=30) - ) - vehicle = Vehicle( - id="vehicle1", - capacity=100, - home_location=location, - departure_time=datetime.now() - ) - plan = VehicleRoutePlan( - name="Test Plan", - south_west_corner=Location(latitude=39.0, longitude=-76.0), - north_east_corner=Location(latitude=41.0, longitude=-74.0), - vehicles=[vehicle], - visits=[visit] - ) - - # Convert to API models - location_model = location_to_model(location) - visit_model = visit_to_model(visit) - vehicle_model = vehicle_to_model(vehicle) - plan_model = plan_to_model(plan) - - print(f"โœ“ Converted Location to model: {location_model}") - print(f"โœ“ Converted Visit to model: {visit_model}") - print(f"โœ“ Converted Vehicle to model: {vehicle_model}") - print(f"โœ“ Converted Plan to model: {plan_model}") - - return True - -if __name__ == "__main__": - try: - test_basic_creation() - test_conversion() - print("\n๐ŸŽ‰ All tests passed! The refactoring is working correctly.") - except Exception as e: - print(f"\nโŒ Test failed: {e}") - import traceback - traceback.print_exc() \ No newline at end of file diff --git a/fast/vehicle-routing-fast/tests/test_constraints.py b/fast/vehicle-routing-fast/tests/test_constraints.py deleted file mode 100644 index fd2e851..0000000 --- a/fast/vehicle-routing-fast/tests/test_constraints.py +++ /dev/null @@ -1,116 +0,0 @@ -from timefold.solver.test import ConstraintVerifier - -from vehicle_routing.domain import * -from vehicle_routing.constraints import * - -from datetime import datetime, timedelta - -# LOCATION_1 to LOCATION_2 is sqrt(3**2 + 4**2) * 4000 == 20_000 seconds of driving time -# LOCATION_2 to LOCATION_3 is sqrt(3**2 + 4**2) * 4000 == 20_000 seconds of driving time -# LOCATION_1 to LOCATION_3 is sqrt(1**2 + 1**2) * 4000 == 5_656 seconds of driving time - -LOCATION_1 = Location(latitude=0, longitude=0) -LOCATION_2 = Location(latitude=3, longitude=4) -LOCATION_3 = Location(latitude=-1, longitude=1) - -DEPARTURE_TIME = datetime(2020, 1, 1) -MIN_START_TIME = DEPARTURE_TIME + timedelta(hours=2) -MAX_END_TIME = DEPARTURE_TIME + timedelta(hours=5) -SERVICE_DURATION = timedelta(hours=1) - -constraint_verifier = ConstraintVerifier.build(define_constraints, VehicleRoutePlan, Vehicle, Visit) - - -def test_vehicle_capacity_unpenalized(): - vehicleA = Vehicle(id="1", capacity=100, home_location=LOCATION_1, departure_time=DEPARTURE_TIME) - visit1 = Visit(id="2", name="John", location=LOCATION_2, demand=80, - min_start_time=MIN_START_TIME, - max_end_time=MAX_END_TIME, - service_duration=SERVICE_DURATION) - connect(vehicleA, visit1) - - (constraint_verifier.verify_that(vehicle_capacity) - .given(vehicleA, visit1) - .penalizes_by(0)) - - -def test_vehicle_capacity_penalized(): - vehicleA = Vehicle(id="1", capacity=100, home_location=LOCATION_1, departure_time=DEPARTURE_TIME) - visit1 = Visit(id="2", name="John", location=LOCATION_2, demand=80, - min_start_time=MIN_START_TIME, - max_end_time=MAX_END_TIME, - service_duration=SERVICE_DURATION) - visit2 = Visit(id="3", name="Paul", location=LOCATION_3, demand=40, - min_start_time=MIN_START_TIME, - max_end_time=MAX_END_TIME, - service_duration=SERVICE_DURATION) - - connect(vehicleA, visit1, visit2) - - (constraint_verifier.verify_that(vehicle_capacity) - .given(vehicleA, visit1, visit2) - .penalizes_by(20)) - - -def test_service_finished_after_max_end_time_unpenalized(): - vehicleA = Vehicle(id="1", capacity=100, home_location=LOCATION_1, departure_time=DEPARTURE_TIME) - visit1 = Visit(id="2", name="John", location=LOCATION_3, demand=80, - min_start_time=MIN_START_TIME, - max_end_time=MAX_END_TIME, - service_duration=SERVICE_DURATION) - - connect(vehicleA, visit1) - - (constraint_verifier.verify_that(service_finished_after_max_end_time) - .given(vehicleA, visit1) - .penalizes_by(0)) - - -def test_service_finished_after_max_end_time_penalized(): - vehicleA = Vehicle(id="1", capacity=100, home_location=LOCATION_1, departure_time=DEPARTURE_TIME) - visit1 = Visit(id="2", name="John", location=LOCATION_2, demand=80, - min_start_time=MIN_START_TIME, - max_end_time=MAX_END_TIME, - service_duration=SERVICE_DURATION) - - connect(vehicleA, visit1) - - # Service duration = 1 hour - # Travel time ~= 5.5 hours - # Max end time = 5 hours after vehicle departure - # So (5.5 + 1) - 5 ~= 1.5 hours penalty, or about 90 minutes - (constraint_verifier.verify_that(service_finished_after_max_end_time) - .given(vehicleA, visit1) - .penalizes_by(94)) - - -def test_total_driving_time(): - vehicleA = Vehicle(id="1", capacity=100, home_location=LOCATION_1, departure_time=DEPARTURE_TIME) - visit1 = Visit(id="2", name="John", location=LOCATION_2, demand=80, - min_start_time=MIN_START_TIME, - max_end_time=MAX_END_TIME, - service_duration=SERVICE_DURATION) - visit2 = Visit(id="3", name="Paul", location=LOCATION_3, demand=40, - min_start_time=MIN_START_TIME, - max_end_time=MAX_END_TIME, - service_duration=SERVICE_DURATION) - - connect(vehicleA, visit1, visit2) - - (constraint_verifier.verify_that(minimize_travel_time) - .given(vehicleA, visit1, visit2) - .penalizes_by(45657) # The sum of the approximate driving time between all three locations. - ) - - -def connect(vehicle: Vehicle, *visits: Visit): - vehicle.visits = list(visits) - for i in range(len(visits)): - visit = visits[i] - visit.vehicle = vehicle - if i > 0: - visit.previous_visit = visits[i - 1] - - if i < len(visits) - 1: - visit.next_visit = visits[i + 1] - visit.update_arrival_time() diff --git a/fast/vehicle-routing-fast/tests/test_feasible.py b/fast/vehicle-routing-fast/tests/test_feasible.py deleted file mode 100644 index 3bd1c99..0000000 --- a/fast/vehicle-routing-fast/tests/test_feasible.py +++ /dev/null @@ -1,30 +0,0 @@ -from vehicle_routing.rest_api import json_to_vehicle_route_plan, app - -from fastapi.testclient import TestClient -from time import sleep -from pytest import fail - -client = TestClient(app) - - -def test_feasible(): - demo_data_response = client.get("/demo-data/PHILADELPHIA") - assert demo_data_response.status_code == 200 - - job_id_response = client.post("/route-plans", json=demo_data_response.json()) - assert job_id_response.status_code == 200 - job_id = job_id_response.text[1:-1] - - ATTEMPTS = 1_000 - for _ in range(ATTEMPTS): - sleep(0.1) - route_plan_response = client.get(f"/route-plans/{job_id}") - route_plan_json = route_plan_response.json() - timetable = json_to_vehicle_route_plan(route_plan_json) - if timetable.score is not None and timetable.score.is_feasible: - stop_solving_response = client.delete(f"/route-plans/{job_id}") - assert stop_solving_response.status_code == 200 - return - - client.delete(f"/route-plans/{job_id}") - fail('solution is not feasible') diff --git a/legacy/deploy/argocd/applicationset.yaml b/legacy/deploy/argocd/applicationset.yaml new file mode 100644 index 0000000..4f30294 --- /dev/null +++ b/legacy/deploy/argocd/applicationset.yaml @@ -0,0 +1,55 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: solverforge-legacy-apps + namespace: argocd +spec: + generators: + - list: + elements: + - appName: employee-scheduling-fast + chartPath: legacy/employee-scheduling-fast/helm/employee-scheduling-fast + - appName: maintenance-scheduling-fast + chartPath: legacy/maintenance-scheduling-fast/helm/maintenance-scheduling-fast + - appName: meeting-scheduling-fast + chartPath: legacy/meeting-scheduling-fast/helm/meeting-scheduling-fast + - appName: order-picking-fast + chartPath: legacy/order-picking-fast/helm/order-picking-fast + - appName: vehicle-routing-fast + chartPath: legacy/vehicle-routing-fast/helm/vehicle-routing-fast + - appName: vm-placement-fast + chartPath: legacy/vm-placement-fast/helm/vm-placement-fast + template: + metadata: + name: '{{appName}}' + namespace: argocd + labels: + app.kubernetes.io/part-of: solverforge-legacy + finalizers: + - resources-finalizer.argocd.argoproj.io + spec: + project: default + source: + repoURL: https://github.com/SolverForge/solverforge-quickstarts.git + targetRevision: HEAD + path: '{{chartPath}}' + helm: + releaseName: '{{appName}}' + valueFiles: + - values.yaml + destination: + server: https://kubernetes.default.svc + namespace: solverforge-benchmark + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - PruneLast=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m diff --git a/legacy/employee-scheduling-fast/Dockerfile b/legacy/employee-scheduling-fast/Dockerfile new file mode 100644 index 0000000..c5369ce --- /dev/null +++ b/legacy/employee-scheduling-fast/Dockerfile @@ -0,0 +1,24 @@ +# Use Python 3.12 base image +FROM python:3.12 + +# Install JDK 21 (required for solverforge-legacy) +RUN apt-get update && \ + apt-get install -y wget gnupg2 && \ + wget -O- https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor > /usr/share/keyrings/adoptium-archive-keyring.gpg && \ + echo "deb [signed-by=/usr/share/keyrings/adoptium-archive-keyring.gpg] https://packages.adoptium.net/artifactory/deb bookworm main" > /etc/apt/sources.list.d/adoptium.list && \ + apt-get update && \ + apt-get install -y temurin-21-jdk && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Copy application files +COPY . . + +# Install the application +RUN pip install --no-cache-dir -e . + +# Expose port 8080 +EXPOSE 8080 + +# Run the application +CMD ["run-app"] diff --git a/fast/employee-scheduling-fast/README.MD b/legacy/employee-scheduling-fast/README.MD similarity index 100% rename from fast/employee-scheduling-fast/README.MD rename to legacy/employee-scheduling-fast/README.MD diff --git a/legacy/employee-scheduling/README.MD b/legacy/employee-scheduling-fast/README.md similarity index 59% rename from legacy/employee-scheduling/README.MD rename to legacy/employee-scheduling-fast/README.md index 036914d..4dff5f9 100644 --- a/legacy/employee-scheduling/README.MD +++ b/legacy/employee-scheduling-fast/README.md @@ -1,16 +1,23 @@ +--- +title: Employee Scheduling (Python) +emoji: ๐Ÿ‘€ +colorFrom: gray +colorTo: green +sdk: docker +app_port: 8080 +pinned: false +license: apache-2.0 +short_description: SolverForge Quickstart for the Employee Scheduling problem +--- + # Employee Scheduling (Python) Schedule shifts to employees, accounting for employee availability and shift skill requirements. -![Employee Scheduling Screenshot](./employee-scheduling-screenshot.png) - - [Prerequisites](#prerequisites) - [Run the application](#run-the-application) - [Test the application](#test-the-application) -> [!TIP] -> [Check out our off-the-shelf model for Employee Shift Scheduling](https://app.timefold.ai/models/employee-scheduling/v1). This model supports many additional constraints such as skills, pairing employees, fairness and more. - ## Prerequisites 1. Install [Python 3.11 or 3.12](https://www.python.org/downloads/). @@ -23,12 +30,12 @@ Schedule shifts to employees, accounting for employee availability and shift ski ## Run the application -1. Git clone the timefold-solver-python repo and navigate to this directory: +1. Git clone the solverforge-solver-python repo and navigate to this directory: ```sh - $ git clone https://github.com/TimefoldAI/timefold-solver-python.git + $ git clone https://github.com/SolverForge/solverforge-quickstarts.git ... - $ cd timefold-solver-python/quickstarts/employee-scheduling + $ cd solverforge-quickstarts/employee-scheduling-fast ``` 2. Create a virtual environment: @@ -69,4 +76,4 @@ Schedule shifts to employees, accounting for employee availability and shift ski ## More information -Visit [timefold.ai](https://timefold.ai). +Visit [solverforge.org](https://www.solverforge.org). diff --git a/fast/employee-scheduling-fast/employee-scheduling-screenshot.png b/legacy/employee-scheduling-fast/employee-scheduling-screenshot.png similarity index 100% rename from fast/employee-scheduling-fast/employee-scheduling-screenshot.png rename to legacy/employee-scheduling-fast/employee-scheduling-screenshot.png diff --git a/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/.helmignore b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/.helmignore new file mode 100644 index 0000000..21846e9 --- /dev/null +++ b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/.helmignore @@ -0,0 +1,17 @@ +.DS_Store +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +*.swp +*.bak +*.tmp +*.orig +*~ +.project +.idea/ +*.tmproj +.vscode/ diff --git a/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/Chart.yaml b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/Chart.yaml new file mode 100644 index 0000000..f4cffc6 --- /dev/null +++ b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v2 +name: employee-scheduling-fast +description: Employee Scheduling optimization using Timefold Solver (Python/FastAPI) +type: application +version: 0.1.0 +appVersion: "1.0.0" +keywords: + - timefold + - optimization + - scheduling + - python + - fastapi diff --git a/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/NOTES.txt b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/NOTES.txt new file mode 100644 index 0000000..09ae9bf --- /dev/null +++ b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/NOTES.txt @@ -0,0 +1,32 @@ +Thank you for installing {{ .Chart.Name }}. + +Your release is named {{ .Release.Name }}. + +To learn more about the release, try: + + $ helm status {{ .Release.Name }} + $ helm get all {{ .Release.Name }} + +{{- if .Values.ingress.enabled }} + +The application is accessible via: +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} + +{{- else }} + +To access the application, run: + + 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}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT + +Then open http://localhost:8080 in your browser. + +{{- end }} + +API Documentation (Swagger UI): http://localhost:8080/q/swagger-ui +Demo Data: http://localhost:8080/demo-data diff --git a/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/_helpers.tpl b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/_helpers.tpl new file mode 100644 index 0000000..2b10a91 --- /dev/null +++ b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/_helpers.tpl @@ -0,0 +1,49 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "app.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "app.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "app.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "app.labels" -}} +helm.sh/chart: {{ include "app.chart" . }} +{{ include "app.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "app.selectorLabels" -}} +app.kubernetes.io/name: {{ include "app.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/deployment.yaml b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/deployment.yaml new file mode 100644 index 0000000..1c0f5ea --- /dev/null +++ b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/deployment.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "app.fullname" . }} + labels: + {{- include "app.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "app.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "app.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/ingress.yaml b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/ingress.yaml new file mode 100644 index 0000000..e6814f0 --- /dev/null +++ b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "app.fullname" . }} + labels: + {{- include "app.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "app.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/service.yaml b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/service.yaml new file mode 100644 index 0000000..a3164fc --- /dev/null +++ b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "app.fullname" . }} + labels: + {{- include "app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "app.selectorLabels" . | nindent 4 }} diff --git a/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/values.yaml b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/values.yaml new file mode 100644 index 0000000..d8b9c2b --- /dev/null +++ b/legacy/employee-scheduling-fast/helm/employee-scheduling-fast/values.yaml @@ -0,0 +1,72 @@ +replicaCount: 1 + +image: + repository: ghcr.io/blackopsrepl/employee-scheduling-fast + pullPolicy: IfNotPresent + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + +securityContext: {} + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: employee-scheduling-fast.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + +resources: + limits: + cpu: 4000m + memory: 4Gi + requests: + cpu: 500m + memory: 512Mi + +livenessProbe: + httpGet: + path: /demo-data + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +readinessProbe: + httpGet: + path: /demo-data + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +env: [] diff --git a/fast/employee-scheduling-fast/logging.conf b/legacy/employee-scheduling-fast/logging.conf similarity index 100% rename from fast/employee-scheduling-fast/logging.conf rename to legacy/employee-scheduling-fast/logging.conf diff --git a/fast/employee-scheduling-fast/pyproject.toml b/legacy/employee-scheduling-fast/pyproject.toml similarity index 78% rename from fast/employee-scheduling-fast/pyproject.toml rename to legacy/employee-scheduling-fast/pyproject.toml index 1592fc4..11a5a80 100644 --- a/fast/employee-scheduling-fast/pyproject.toml +++ b/legacy/employee-scheduling-fast/pyproject.toml @@ -5,10 +5,10 @@ build-backend = "hatchling.build" [project] name = "employee_scheduling" -version = "1.0.0" -requires-python = ">=3.11" +version = "1.0.1" +requires-python = ">=3.10" dependencies = [ - 'timefold == 1.24.0b0', + 'solverforge-legacy == 1.24.1', 'fastapi == 0.111.0', 'pydantic == 2.7.3', 'uvicorn == 0.30.1', diff --git a/legacy/employee-scheduling-fast/src/employee_scheduling/.project b/legacy/employee-scheduling-fast/src/employee_scheduling/.project new file mode 100644 index 0000000..9a2961c --- /dev/null +++ b/legacy/employee-scheduling-fast/src/employee_scheduling/.project @@ -0,0 +1 @@ +/srv/lab/dev/solverforge/solverforge-quickstarts/fast/employee-scheduling-fast/src/employee_scheduling \ No newline at end of file diff --git a/legacy/employee-scheduling-fast/src/employee_scheduling/__init__.py b/legacy/employee-scheduling-fast/src/employee_scheduling/__init__.py new file mode 100644 index 0000000..1922abb --- /dev/null +++ b/legacy/employee-scheduling-fast/src/employee_scheduling/__init__.py @@ -0,0 +1,19 @@ +import uvicorn + +from .rest_api import app as app + + +def main(): + config = uvicorn.Config( + "employee_scheduling:app", + host="0.0.0.0", + port=8080, + log_config="logging.conf", + use_colors=True, + ) + server = uvicorn.Server(config) + server.run() + + +if __name__ == "__main__": + main() diff --git a/legacy/employee-scheduling-fast/src/employee_scheduling/constraints.py b/legacy/employee-scheduling-fast/src/employee_scheduling/constraints.py new file mode 100644 index 0000000..f309576 --- /dev/null +++ b/legacy/employee-scheduling-fast/src/employee_scheduling/constraints.py @@ -0,0 +1,212 @@ +from solverforge_legacy.solver.score import ( + constraint_provider, + ConstraintFactory, + Joiners, + HardSoftDecimalScore, + ConstraintCollectors, +) +from datetime import datetime, date, time + +from .domain import Employee, Shift + + +def get_minute_overlap(shift1: Shift, shift2: Shift) -> int: + return ( + min(shift1.end, shift2.end) - max(shift1.start, shift2.start) + ).total_seconds() // 60 + + +def is_overlapping_with_date(shift: Shift, dt: date) -> bool: + return shift.start.date() == dt or shift.end.date() == dt + + +def overlapping_in_minutes( + first_start_datetime: datetime, + first_end_datetime: datetime, + second_start_datetime: datetime, + second_end_datetime: datetime, +) -> int: + latest_start = max(first_start_datetime, second_start_datetime) + earliest_end = min(first_end_datetime, second_end_datetime) + delta = (earliest_end - latest_start).total_seconds() / 60 + return max(0, delta) + + +def get_shift_overlapping_duration_in_minutes(shift: Shift, dt: date) -> int: + start_date_time = datetime.combine(dt, datetime.min.time()) + end_date_time = datetime.combine(dt, datetime.max.time()) + overlap = overlapping_in_minutes( + start_date_time, end_date_time, shift.start, shift.end + ) + return int(overlap) + + +@constraint_provider +def define_constraints(constraint_factory: ConstraintFactory): + return [ + # Hard constraints + required_skill(constraint_factory), + no_overlapping_shifts(constraint_factory), + at_least_10_hours_between_two_shifts(constraint_factory), + one_shift_per_day(constraint_factory), + unavailable_employee(constraint_factory), + # max_shifts_per_employee(constraint_factory), # Optional extension - disabled by default + # Soft constraints + undesired_day_for_employee(constraint_factory), + desired_day_for_employee(constraint_factory), + balance_employee_shift_assignments(constraint_factory), + ] + + +def required_skill(constraint_factory: ConstraintFactory): + return ( + constraint_factory.for_each(Shift) + .filter(lambda shift: not shift.has_required_skill()) + .penalize(HardSoftDecimalScore.ONE_HARD) + .as_constraint("Missing required skill") + ) + + +def no_overlapping_shifts(constraint_factory: ConstraintFactory): + return ( + constraint_factory.for_each_unique_pair( + Shift, + Joiners.equal(lambda shift: shift.employee.name), + Joiners.overlapping(lambda shift: shift.start, lambda shift: shift.end), + ) + .penalize(HardSoftDecimalScore.ONE_HARD, get_minute_overlap) + .as_constraint("Overlapping shift") + ) + + +def at_least_10_hours_between_two_shifts(constraint_factory: ConstraintFactory): + return ( + constraint_factory.for_each(Shift) + .join( + Shift, + Joiners.equal(lambda shift: shift.employee.name), + Joiners.less_than_or_equal( + lambda shift: shift.end, lambda shift: shift.start + ), + ) + .filter( + lambda first_shift, second_shift: ( + second_shift.start - first_shift.end + ).total_seconds() + // (60 * 60) + < 10 + ) + .penalize( + HardSoftDecimalScore.ONE_HARD, + lambda first_shift, second_shift: 600 + - ((second_shift.start - first_shift.end).total_seconds() // 60), + ) + .as_constraint("At least 10 hours between 2 shifts") + ) + + +def one_shift_per_day(constraint_factory: ConstraintFactory): + return ( + constraint_factory.for_each_unique_pair( + Shift, + Joiners.equal(lambda shift: shift.employee.name), + Joiners.equal(lambda shift: shift.start.date()), + ) + .penalize(HardSoftDecimalScore.ONE_HARD) + .as_constraint("Max one shift per day") + ) + + +def unavailable_employee(constraint_factory: ConstraintFactory): + return ( + constraint_factory.for_each(Shift) + .join( + Employee, + Joiners.equal(lambda shift: shift.employee, lambda employee: employee), + ) + .flatten_last(lambda employee: employee.unavailable_dates) + .filter(lambda shift, unavailable_date: is_overlapping_with_date(shift, unavailable_date)) + .penalize( + HardSoftDecimalScore.ONE_HARD, + lambda shift, unavailable_date: int((min(shift.end, datetime.combine(unavailable_date, time(23, 59, 59))) - max(shift.start, datetime.combine(unavailable_date, time(0, 0, 0)))).total_seconds() / 60), + ) + .as_constraint("Unavailable employee") + ) + + +def max_shifts_per_employee(constraint_factory: ConstraintFactory): + """ + Hard constraint: No employee can have more than 12 shifts. + + The limit of 12 is chosen based on the demo data dimensions: + - SMALL dataset: 139 shifts / 15 employees = ~9.3 average + - This provides headroom while preventing extreme imbalance + + Note: A limit that's too low (e.g., 5) would make the problem infeasible. + Always ensure your constraints are compatible with your data dimensions. + """ + return ( + constraint_factory.for_each(Shift) + .group_by(lambda shift: shift.employee, ConstraintCollectors.count()) + .filter(lambda employee, shift_count: shift_count > 12) + .penalize( + HardSoftDecimalScore.ONE_HARD, + lambda employee, shift_count: shift_count - 12, + ) + .as_constraint("Max 12 shifts per employee") + ) + + +def undesired_day_for_employee(constraint_factory: ConstraintFactory): + return ( + constraint_factory.for_each(Shift) + .join( + Employee, + Joiners.equal(lambda shift: shift.employee, lambda employee: employee), + ) + .flatten_last(lambda employee: employee.undesired_dates) + .filter(lambda shift, undesired_date: shift.is_overlapping_with_date(undesired_date)) + .penalize( + HardSoftDecimalScore.ONE_SOFT, + lambda shift, undesired_date: int((min(shift.end, datetime.combine(undesired_date, time(23, 59, 59))) - max(shift.start, datetime.combine(undesired_date, time(0, 0, 0)))).total_seconds() / 60), + ) + .as_constraint("Undesired day for employee") + ) + + +def desired_day_for_employee(constraint_factory: ConstraintFactory): + return ( + constraint_factory.for_each(Shift) + .join( + Employee, + Joiners.equal(lambda shift: shift.employee, lambda employee: employee), + ) + .flatten_last(lambda employee: employee.desired_dates) + .filter(lambda shift, desired_date: shift.is_overlapping_with_date(desired_date)) + .reward( + HardSoftDecimalScore.ONE_SOFT, + lambda shift, desired_date: int((min(shift.end, datetime.combine(desired_date, time(23, 59, 59))) - max(shift.start, datetime.combine(desired_date, time(0, 0, 0)))).total_seconds() / 60), + ) + .as_constraint("Desired day for employee") + ) + + +def balance_employee_shift_assignments(constraint_factory: ConstraintFactory): + return ( + constraint_factory.for_each(Shift) + .group_by(lambda shift: shift.employee, ConstraintCollectors.count()) + .complement( + Employee, lambda e: 0 + ) # Include all employees which are not assigned to any shift. + .group_by( + ConstraintCollectors.load_balance( + lambda employee, shift_count: employee, + lambda employee, shift_count: shift_count, + ) + ) + .penalize_decimal( + HardSoftDecimalScore.ONE_SOFT, + lambda load_balance: load_balance.unfairness(), + ) + .as_constraint("Balance employee shift assignments") + ) diff --git a/fast/employee-scheduling-fast/src/employee_scheduling/converters.py b/legacy/employee-scheduling-fast/src/employee_scheduling/converters.py similarity index 82% rename from fast/employee-scheduling-fast/src/employee_scheduling/converters.py rename to legacy/employee-scheduling-fast/src/employee_scheduling/converters.py index 22b5446..e66288e 100644 --- a/fast/employee-scheduling-fast/src/employee_scheduling/converters.py +++ b/legacy/employee-scheduling-fast/src/employee_scheduling/converters.py @@ -1,8 +1,5 @@ -from typing import List, Optional, Union from datetime import datetime, date from . import domain -from .json_serialization import JsonDomainBase -from pydantic import Field # Conversion functions from domain to API models @@ -12,7 +9,7 @@ def employee_to_model(employee: domain.Employee) -> domain.EmployeeModel: skills=list(employee.skills), unavailable_dates=[d.isoformat() for d in employee.unavailable_dates], undesired_dates=[d.isoformat() for d in employee.undesired_dates], - desired_dates=[d.isoformat() for d in employee.desired_dates] + desired_dates=[d.isoformat() for d in employee.desired_dates], ) @@ -23,16 +20,18 @@ def shift_to_model(shift: domain.Shift) -> domain.ShiftModel: end=shift.end.isoformat(), location=shift.location, required_skill=shift.required_skill, - employee=employee_to_model(shift.employee) if shift.employee else None + employee=employee_to_model(shift.employee) if shift.employee else None, ) -def schedule_to_model(schedule: domain.EmployeeSchedule) -> domain.EmployeeScheduleModel: +def schedule_to_model( + schedule: domain.EmployeeSchedule, +) -> domain.EmployeeScheduleModel: return domain.EmployeeScheduleModel( employees=[employee_to_model(e) for e in schedule.employees], shifts=[shift_to_model(s) for s in schedule.shifts], score=str(schedule.score) if schedule.score else None, - solver_status=schedule.solver_status.name if schedule.solver_status else None + solver_status=schedule.solver_status.name if schedule.solver_status else None, ) @@ -43,7 +42,7 @@ def model_to_employee(model: domain.EmployeeModel) -> domain.Employee: skills=set(model.skills), unavailable_dates={date.fromisoformat(d) for d in model.unavailable_dates}, undesired_dates={date.fromisoformat(d) for d in model.undesired_dates}, - desired_dates={date.fromisoformat(d) for d in model.desired_dates} + desired_dates={date.fromisoformat(d) for d in model.desired_dates}, ) @@ -55,44 +54,39 @@ def model_to_shift(model: domain.ShiftModel, employee_lookup: dict) -> domain.Sh employee = employee_lookup[model.employee] else: employee = model_to_employee(model.employee) - + return domain.Shift( id=model.id, start=datetime.fromisoformat(model.start), end=datetime.fromisoformat(model.end), location=model.location, required_skill=model.required_skill, - employee=employee + employee=employee, ) def model_to_schedule(model: domain.EmployeeScheduleModel) -> domain.EmployeeSchedule: # Convert employees first employees = [model_to_employee(e) for e in model.employees] - + # Create lookup dictionary for employee references employee_lookup = {e.name: e for e in employees} - + # Convert shifts with employee lookups - shifts = [ - model_to_shift(s, employee_lookup) - for s in model.shifts - ] - + shifts = [model_to_shift(s, employee_lookup) for s in model.shifts] + # Handle score score = None if model.score: - from timefold.solver.score import HardSoftDecimalScore + from solverforge_legacy.solver.score import HardSoftDecimalScore + score = HardSoftDecimalScore.parse(model.score) - + # Handle solver status solver_status = domain.SolverStatus.NOT_SOLVING if model.solver_status: solver_status = domain.SolverStatus[model.solver_status] - + return domain.EmployeeSchedule( - employees=employees, - shifts=shifts, - score=score, - solver_status=solver_status - ) \ No newline at end of file + employees=employees, shifts=shifts, score=score, solver_status=solver_status + ) diff --git a/legacy/employee-scheduling/src/employee_scheduling/demo_data.py b/legacy/employee-scheduling-fast/src/employee_scheduling/demo_data.py similarity index 99% rename from legacy/employee-scheduling/src/employee_scheduling/demo_data.py rename to legacy/employee-scheduling-fast/src/employee_scheduling/demo_data.py index 4c8bc1f..d36066a 100644 --- a/legacy/employee-scheduling/src/employee_scheduling/demo_data.py +++ b/legacy/employee-scheduling-fast/src/employee_scheduling/demo_data.py @@ -5,7 +5,7 @@ from typing import Generator from dataclasses import dataclass, field -from .domain import * +from .domain import Employee, EmployeeSchedule, Shift class DemoData(Enum): diff --git a/fast/employee-scheduling-fast/src/employee_scheduling/domain.py b/legacy/employee-scheduling-fast/src/employee_scheduling/domain.py similarity index 55% rename from fast/employee-scheduling-fast/src/employee_scheduling/domain.py rename to legacy/employee-scheduling-fast/src/employee_scheduling/domain.py index 794dade..4622ab8 100644 --- a/fast/employee-scheduling-fast/src/employee_scheduling/domain.py +++ b/legacy/employee-scheduling-fast/src/employee_scheduling/domain.py @@ -1,10 +1,15 @@ -from timefold.solver import SolverStatus -from timefold.solver.domain import ( - planning_entity, planning_solution, PlanningId, PlanningVariable, - PlanningEntityCollectionProperty, ProblemFactCollectionProperty, ValueRangeProvider, - PlanningScore +from solverforge_legacy.solver import SolverStatus +from solverforge_legacy.solver.domain import ( + planning_entity, + planning_solution, + PlanningId, + PlanningVariable, + PlanningEntityCollectionProperty, + ProblemFactCollectionProperty, + ValueRangeProvider, + PlanningScore, ) -from timefold.solver.score import HardSoftDecimalScore +from solverforge_legacy.solver.score import HardSoftDecimalScore from datetime import datetime, date from typing import Annotated, List, Optional, Union from dataclasses import dataclass, field @@ -31,11 +36,35 @@ class Shift: required_skill: str employee: Annotated[Employee | None, PlanningVariable] = None + def has_required_skill(self) -> bool: + """Check if assigned employee has the required skill.""" + if self.employee is None: + return False + return self.required_skill in self.employee.skills + + def is_overlapping_with_date(self, dt: date) -> bool: + """Check if shift overlaps with a specific date.""" + return self.start.date() == dt or self.end.date() == dt + + def get_overlapping_duration_in_minutes(self, dt: date) -> int: + """Calculate overlap duration in minutes for a specific date.""" + start_date_time = datetime.combine(dt, datetime.min.time()) + end_date_time = datetime.combine(dt, datetime.max.time()) + + # Calculate overlap between date range and shift range + max_start_time = max(start_date_time, self.start) + min_end_time = min(end_date_time, self.end) + + minutes = (min_end_time - max_start_time).total_seconds() / 60 + return int(max(0, minutes)) + @planning_solution @dataclass class EmployeeSchedule: - employees: Annotated[list[Employee], ProblemFactCollectionProperty, ValueRangeProvider] + employees: Annotated[ + list[Employee], ProblemFactCollectionProperty, ValueRangeProvider + ] shifts: Annotated[list[Shift], PlanningEntityCollectionProperty] score: Annotated[HardSoftDecimalScore | None, PlanningScore] = None solver_status: SolverStatus = SolverStatus.NOT_SOLVING @@ -53,7 +82,7 @@ class EmployeeModel(JsonDomainBase): class ShiftModel(JsonDomainBase): id: str start: str # ISO datetime string - end: str # ISO datetime string + end: str # ISO datetime string location: str required_skill: str = Field(..., alias="requiredSkill") employee: Union[str, EmployeeModel, None] = None diff --git a/fast/employee-scheduling-fast/src/employee_scheduling/json_serialization.py b/legacy/employee-scheduling-fast/src/employee_scheduling/json_serialization.py similarity index 76% rename from fast/employee-scheduling-fast/src/employee_scheduling/json_serialization.py rename to legacy/employee-scheduling-fast/src/employee_scheduling/json_serialization.py index 58bafbe..a919e96 100644 --- a/fast/employee-scheduling-fast/src/employee_scheduling/json_serialization.py +++ b/legacy/employee-scheduling-fast/src/employee_scheduling/json_serialization.py @@ -1,9 +1,11 @@ -from timefold.solver.score import HardSoftDecimalScore +from solverforge_legacy.solver.score import HardSoftDecimalScore from typing import Any from pydantic import BaseModel, ConfigDict, PlainSerializer, BeforeValidator from pydantic.alias_generators import to_camel -ScoreSerializer = PlainSerializer(lambda score: str(score) if score is not None else None, return_type=str | None) +ScoreSerializer = PlainSerializer( + lambda score: str(score) if score is not None else None, return_type=str | None +) def validate_score(v: Any) -> Any: diff --git a/legacy/employee-scheduling-fast/src/employee_scheduling/rest_api.py b/legacy/employee-scheduling-fast/src/employee_scheduling/rest_api.py new file mode 100644 index 0000000..303452f --- /dev/null +++ b/legacy/employee-scheduling-fast/src/employee_scheduling/rest_api.py @@ -0,0 +1,128 @@ +from fastapi import FastAPI, Request +from fastapi.staticfiles import StaticFiles +from uuid import uuid4 +from dataclasses import replace +from typing import Dict, List + +from .domain import EmployeeSchedule, EmployeeScheduleModel +from .converters import ( + schedule_to_model, model_to_schedule +) +from .demo_data import DemoData, generate_demo_data +from .solver import solver_manager, solution_manager +from .score_analysis import ConstraintAnalysisDTO, MatchAnalysisDTO + +app = FastAPI(docs_url='/q/swagger-ui') +data_sets: dict[str, EmployeeSchedule] = {} + + +@app.get("/demo-data") +async def demo_data_list() -> list[DemoData]: + return [e for e in DemoData] + + +@app.get("/demo-data/{dataset_id}", response_model_exclude_none=True) +async def get_demo_data(dataset_id: str) -> EmployeeScheduleModel: + demo_data = getattr(DemoData, dataset_id) + domain_schedule = generate_demo_data(demo_data) + return schedule_to_model(domain_schedule) + + +@app.get("/schedules/{problem_id}", response_model_exclude_none=True) +async def get_timetable(problem_id: str) -> EmployeeScheduleModel: + schedule = data_sets[problem_id] + updated_schedule = replace(schedule, solver_status=solver_manager.get_solver_status(problem_id)) + return schedule_to_model(updated_schedule) + + +def update_schedule(problem_id: str, schedule: EmployeeSchedule): + global data_sets + data_sets[problem_id] = schedule + + +@app.post("/schedules") +async def solve_timetable(schedule_model: EmployeeScheduleModel) -> str: + job_id = str(uuid4()) + schedule = model_to_schedule(schedule_model) + data_sets[job_id] = schedule + solver_manager.solve_and_listen(job_id, schedule, + lambda solution: update_schedule(job_id, solution)) + return job_id + + +@app.get("/schedules") +async def list_schedules() -> List[str]: + """List all job IDs of submitted schedules.""" + return list(data_sets.keys()) + + +@app.get("/schedules/{problem_id}/status") +async def get_status(problem_id: str) -> Dict: + """Get the schedule status and score for a given job ID.""" + if problem_id not in data_sets: + raise ValueError(f"No schedule found with ID {problem_id}") + + schedule = data_sets[problem_id] + solver_status = solver_manager.get_solver_status(problem_id) + + return { + "score": { + "hardScore": schedule.score.hard_score if schedule.score else 0, + "softScore": schedule.score.soft_score if schedule.score else 0, + }, + "solverStatus": solver_status.name, + } + + +@app.delete("/schedules/{problem_id}") +async def stop_solving(problem_id: str) -> EmployeeScheduleModel: + """Terminate solving for a given job ID.""" + if problem_id not in data_sets: + raise ValueError(f"No schedule found with ID {problem_id}") + + try: + solver_manager.terminate_early(problem_id) + except Exception as e: + print(f"Warning: terminate_early failed for {problem_id}: {e}") + + return await get_timetable(problem_id) + + +@app.put("/schedules/analyze") +async def analyze_schedule(request: Request) -> Dict: + """Submit a schedule to analyze its score.""" + json_data = await request.json() + + # Parse the incoming JSON using Pydantic models + schedule_model = EmployeeScheduleModel.model_validate(json_data) + + # Convert to domain model for analysis + domain_schedule = model_to_schedule(schedule_model) + + analysis = solution_manager.analyze(domain_schedule) + + # Convert to proper DTOs for correct serialization + # Use str() for scores and justification to avoid Java object serialization issues + constraints = [] + for constraint in getattr(analysis, 'constraint_analyses', []) or []: + matches = [ + MatchAnalysisDTO( + name=str(getattr(getattr(match, 'constraint_ref', None), 'constraint_name', "")), + score=str(getattr(match, 'score', "0hard/0soft")), + justification=str(getattr(match, 'justification', "")), + ) + for match in getattr(constraint, 'matches', []) or [] + ] + + constraint_dto = ConstraintAnalysisDTO( + name=str(getattr(constraint, 'constraint_name', "")), + weight=str(getattr(constraint, 'weight', "0hard/0soft")), + score=str(getattr(constraint, 'score', "0hard/0soft")), + matches=matches, + ) + constraints.append(constraint_dto) + + return {"constraints": [constraint.model_dump() for constraint in constraints]} + + +app.mount("/", StaticFiles(directory="static", html=True), name="static") diff --git a/legacy/employee-scheduling-fast/src/employee_scheduling/score_analysis.py b/legacy/employee-scheduling-fast/src/employee_scheduling/score_analysis.py new file mode 100644 index 0000000..1d33a43 --- /dev/null +++ b/legacy/employee-scheduling-fast/src/employee_scheduling/score_analysis.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel +from typing import List + + +class MatchAnalysisDTO(BaseModel): + name: str + score: str + justification: str + + +class ConstraintAnalysisDTO(BaseModel): + name: str + weight: str + score: str + matches: List[MatchAnalysisDTO] diff --git a/fast/employee-scheduling-fast/src/employee_scheduling/solver.py b/legacy/employee-scheduling-fast/src/employee_scheduling/solver.py similarity index 59% rename from fast/employee-scheduling-fast/src/employee_scheduling/solver.py rename to legacy/employee-scheduling-fast/src/employee_scheduling/solver.py index 765eb74..54292a1 100644 --- a/fast/employee-scheduling-fast/src/employee_scheduling/solver.py +++ b/legacy/employee-scheduling-fast/src/employee_scheduling/solver.py @@ -1,6 +1,10 @@ -from timefold.solver import SolverManager, SolverFactory, SolutionManager -from timefold.solver.config import (SolverConfig, ScoreDirectorFactoryConfig, - TerminationConfig, Duration) +from solverforge_legacy.solver import SolverManager, SolverFactory, SolutionManager +from solverforge_legacy.solver.config import ( + SolverConfig, + ScoreDirectorFactoryConfig, + TerminationConfig, + Duration, +) from .domain import EmployeeSchedule, Shift from .constraints import define_constraints @@ -12,9 +16,7 @@ score_director_factory_config=ScoreDirectorFactoryConfig( constraint_provider_function=define_constraints ), - termination_config=TerminationConfig( - spent_limit=Duration(seconds=30) - ) + termination_config=TerminationConfig(spent_limit=Duration(seconds=30)), ) solver_manager = SolverManager.create(SolverFactory.create(solver_config)) diff --git a/fast/employee-scheduling-fast/static/app.js b/legacy/employee-scheduling-fast/static/app.js similarity index 87% rename from fast/employee-scheduling-fast/static/app.js rename to legacy/employee-scheduling-fast/static/app.js index 3227d27..fe978ee 100644 --- a/fast/employee-scheduling-fast/static/app.js +++ b/legacy/employee-scheduling-fast/static/app.js @@ -39,7 +39,24 @@ let windowStart = JSJoda.LocalDate.now().toString(); let windowEnd = JSJoda.LocalDate.parse(windowStart).plusDays(7).toString(); $(document).ready(function () { - replaceQuickstartTimefoldAutoHeaderFooter(); + let initialized = false; + + function safeInitialize() { + if (!initialized) { + initialized = true; + initializeApp(); + } + } + + // Ensure all resources are loaded before initializing + $(window).on('load', safeInitialize); + + // Fallback if window load event doesn't fire + setTimeout(safeInitialize, 100); +}); + +function initializeApp() { + replaceQuickstartSolverForgeAutoHeaderFooter(); $("#solveButton").click(function () { solve(); @@ -60,7 +77,7 @@ $(document).ready(function () { setupAjax(); fetchDemoData(); -}); +} function setupAjax() { $.ajaxSetup({ @@ -163,12 +180,25 @@ function refreshSchedule() { } function renderSchedule(schedule) { + console.log('Rendering schedule:', schedule); + + if (!schedule) { + console.error('No schedule data provided to renderSchedule'); + return; + } + refreshSolvingButtons(schedule.solverStatus != null && schedule.solverStatus !== "NOT_SOLVING"); $("#score").text("Score: " + (schedule.score == null ? "?" : schedule.score)); const unassignedShifts = $("#unassignedShifts"); const groups = []; + // Check if schedule.shifts exists and is an array + if (!schedule.shifts || !Array.isArray(schedule.shifts) || schedule.shifts.length === 0) { + console.warn('No shifts data available in schedule'); + return; + } + // Show only first 7 days of draft const scheduleStart = schedule.shifts.map(shift => JSJoda.LocalDateTime.parse(shift.start).toLocalDate()).sort()[0].toString(); const scheduleEnd = JSJoda.LocalDate.parse(scheduleStart).plusDays(7).toString(); @@ -184,6 +214,11 @@ function renderSchedule(schedule) { byEmployeeItemDataSet.clear(); byLocationItemDataSet.clear(); + // Check if schedule.employees exists and is an array + if (!schedule.employees || !Array.isArray(schedule.employees)) { + console.warn('No employees data available in schedule'); + return; + } schedule.employees.forEach((employee, index) => { const employeeGroupElement = $('
') @@ -301,6 +336,12 @@ function renderSchedule(schedule) { } function solve() { + if (!loadedSchedule) { + showError("No schedule data loaded. Please wait for the data to load or refresh the page."); + return; + } + + console.log('Sending schedule data for solving:', loadedSchedule); $.post("/schedules", JSON.stringify(loadedSchedule), function (data) { scheduleId = data; refreshSolvingButtons(true); @@ -399,29 +440,14 @@ function refreshSolvingButtons(solving) { if (solving) { $("#solveButton").hide(); $("#stopSolvingButton").show(); + $("#solvingSpinner").addClass("active"); if (autoRefreshIntervalId == null) { autoRefreshIntervalId = setInterval(refreshSchedule, 2000); } } else { $("#solveButton").show(); $("#stopSolvingButton").hide(); - if (autoRefreshIntervalId != null) { - clearInterval(autoRefreshIntervalId); - autoRefreshIntervalId = null; - } - } -} - -function refreshSolvingButtons(solving) { - if (solving) { - $("#solveButton").hide(); - $("#stopSolvingButton").show(); - if (autoRefreshIntervalId == null) { - autoRefreshIntervalId = setInterval(refreshSchedule, 2000); - } - } else { - $("#solveButton").show(); - $("#stopSolvingButton").hide(); + $("#solvingSpinner").removeClass("active"); if (autoRefreshIntervalId != null) { clearInterval(autoRefreshIntervalId); autoRefreshIntervalId = null; @@ -438,15 +464,15 @@ function stopSolving() { }); } -function replaceQuickstartTimefoldAutoHeaderFooter() { - const timefoldHeader = $("header#timefold-auto-header"); - if (timefoldHeader != null) { - timefoldHeader.addClass("bg-black") - timefoldHeader.append( +function replaceQuickstartSolverForgeAutoHeaderFooter() { + const solverforgeHeader = $("header#solverforge-auto-header"); + if (solverforgeHeader != null) { + solverforgeHeader.css("background-color", "#ffffff"); + solverforgeHeader.append( $(`
-
`)); } - const timefoldFooter = $("footer#timefold-auto-footer"); - if (timefoldFooter != null) { - timefoldFooter.append( + const solverforgeFooter = $("footer#solverforge-auto-footer"); + if (solverforgeFooter != null) { + solverforgeFooter.append( $(``)); diff --git a/legacy/employee-scheduling/static/index.html b/legacy/employee-scheduling-fast/static/index.html similarity index 75% rename from legacy/employee-scheduling/static/index.html rename to legacy/employee-scheduling-fast/static/index.html index cf57b81..f94c30f 100644 --- a/legacy/employee-scheduling/static/index.html +++ b/legacy/employee-scheduling-fast/static/index.html @@ -2,24 +2,42 @@ - Employee scheduling - Timefold Solver for Python + Employee scheduling - SolverForge for Python - + - + -
+
@@ -38,8 +56,12 @@

Employee scheduling solver

+ Score: ? +
-
+ + + +
- + - \ No newline at end of file + diff --git a/legacy/meeting-scheduling/static/webjars/timefold/css/timefold-webui.css b/legacy/employee-scheduling-fast/static/webjars/solverforge/css/solverforge-webui.css similarity index 89% rename from legacy/meeting-scheduling/static/webjars/timefold/css/timefold-webui.css rename to legacy/employee-scheduling-fast/static/webjars/solverforge/css/solverforge-webui.css index 0d729db..fce2266 100644 --- a/legacy/meeting-scheduling/static/webjars/timefold/css/timefold-webui.css +++ b/legacy/employee-scheduling-fast/static/webjars/solverforge/css/solverforge-webui.css @@ -2,6 +2,8 @@ /* Keep in sync with .navbar height on a large screen. */ --ts-navbar-height: 109px; + --ts-green-1-rgb: #10b981; + --ts-green-2-rgb: #059669; --ts-violet-1-rgb: #3E00FF; --ts-violet-2-rgb: #3423A6; --ts-violet-3-rgb: #2E1760; @@ -56,5 +58,11 @@ --bs-navbar-hover-color: var(--ts-white-rgb); } .nav-pills { - --bs-nav-pills-link-active-bg: var(--ts-violet-1-rgb); + --bs-nav-pills-link-active-bg: var(--ts-green-1-rgb); +} +.nav-pills .nav-link:hover { + color: var(--ts-green-1-rgb); +} +.nav-pills .nav-link.active:hover { + color: var(--ts-white-rgb); } diff --git a/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-favicon.svg b/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-favicon.svg new file mode 100644 index 0000000..e3a8e9d --- /dev/null +++ b/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-favicon.svg @@ -0,0 +1,65 @@ + + + + +SolverForge Optimization Software + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-horizontal-white.svg b/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-horizontal-white.svg new file mode 100644 index 0000000..1f49408 --- /dev/null +++ b/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-horizontal-white.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SolverForge + + + OPTIMIZATION SOFTWARE + + + + + diff --git a/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-horizontal.svg b/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-horizontal.svg new file mode 100644 index 0000000..e3932bf --- /dev/null +++ b/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-horizontal.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SolverForge + + + OPTIMIZATION SOFTWARE + + + + + diff --git a/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-logo-stacked.svg b/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-logo-stacked.svg new file mode 100644 index 0000000..33012de --- /dev/null +++ b/legacy/employee-scheduling-fast/static/webjars/solverforge/img/solverforge-logo-stacked.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SolverForge + + + OPTIMIZATION SOFTWARE + + + + + diff --git a/legacy/meeting-scheduling/static/webjars/timefold/js/timefold-webui.js b/legacy/employee-scheduling-fast/static/webjars/solverforge/js/solverforge-webui.js similarity index 86% rename from legacy/meeting-scheduling/static/webjars/timefold/js/timefold-webui.js rename to legacy/employee-scheduling-fast/static/webjars/solverforge/js/solverforge-webui.js index 09f1f6a..10507b3 100644 --- a/legacy/meeting-scheduling/static/webjars/timefold/js/timefold-webui.js +++ b/legacy/employee-scheduling-fast/static/webjars/solverforge/js/solverforge-webui.js @@ -1,29 +1,29 @@ -function replaceTimefoldAutoHeaderFooter() { - const timefoldHeader = $("header#timefold-auto-header"); - if (timefoldHeader != null) { - timefoldHeader.addClass("bg-black") - timefoldHeader.append( +function replaceSolverForgeAutoHeaderFooter() { + const solverforgeHeader = $("header#solverforge-auto-header"); + if (solverforgeHeader != null) { + solverforgeHeader.addClass("bg-black") + solverforgeHeader.append( $(``)); } - const timefoldFooter = $("footer#timefold-auto-footer"); - if (timefoldFooter != null) { - timefoldFooter.append( + const solverforgeFooter = $("footer#solverforge-auto-footer"); + if (solverforgeFooter != null) { + solverforgeFooter.append( $(`