| name | schedule-optimization |
| description | Multi-objective schedule optimization expertise using constraint programming and Pareto optimization. Use when generating schedules, improving coverage, balancing workloads, or resolving conflicts. Integrates with OR-Tools solver and resilience framework. |
Schedule Optimization Skill
Expert knowledge for generating and optimizing medical residency schedules using constraint programming and multi-objective optimization.
Solver Status (2025-12-24, Updated 2025-12-26)
| Issue | Status | Fix Applied |
|---|---|---|
| Greedy template selection | FIXED | Selects template with fewest assignments |
| CP-SAT no template balance | FIXED | Added template_balance_penalty to objective |
| Template filtering missing | FIXED | _get_rotation_templates() defaults to activity_type="outpatient" |
NOTE (2025-12-26): The template filtering was initially set to "clinic" which was incorrect.
PR #442 was not merged because this issue was caught during evaluation. The correct filter
is "outpatient" because that matches the elective/selective templates that use half-day
scheduling. The "clinic" activity_type is specifically for FM Clinic which has its own
capacity and supervision constraint logic.
See backend/app/scheduling/solvers.py header for implementation details.
Architecture: Block vs Half-Day Scheduling
IMPORTANT: This system has two distinct scheduling modes:
| Mode | Rotations | Assignment Unit | Solver Role |
|---|---|---|---|
| Block-Assigned | FMIT, NF, Inpatient, NICU | Full block or half-block | Pre-assigned, NOT optimized |
| Half-Day Optimized | Clinic, Specialty | Half-day (AM/PM) | Solver optimizes these |
The solvers are ONLY for outpatient half-day optimization. Block-assigned rotations are handled separately and should NOT be passed to the solver.
If solver assigns everyone to NF/PC/inpatient, check that templates are filtered
to activity_type == "outpatient" in engine._get_rotation_templates().
Activity Types Clarification:
| Activity Type | Templates | For Solver? |
|---|---|---|
outpatient |
Neurology, ID, Palliative, PedsSub, etc. | YES - half-day electives |
clinic |
Family Medicine Clinic (FMC) | NO - has separate capacity constraints |
inpatient |
FMIT, IM, EM, L&D | NO - block-assigned |
night_float |
NF, NICU+NF, etc. | NO - block-assigned |
procedure |
Procedures Rotation | Depends on configuration |
Night Float (NF) Half-Block Mirrored Pairing
NF has idiosyncratic half-block constraints - residents are paired in mirrored patterns:
Block 5 (4 weeks):
├── Half 1 (Days 1-14) ├── Half 2 (Days 15-28)
│ │
│ Resident A: NF │ Resident A: NICU (or elective)
│ Resident B: NEURO │ Resident B: NF
Key rules:
- NF is assigned per half-block (2 weeks), not full block
- Residents are mirrored pairs: one on NF half 1, partner on NF half 2
- The non-NF half is a mini 2-week rotation (NICU, NEURO, elective)
- Post-Call (PC) day required after NF ends (Day 15 or Day 1 of next block)
- Exactly 1 resident on NF per half-block
Files: See backend/app/scheduling/constraints/night_float_post_call.py and
docs/development/CODEX_SYSTEM_OVERVIEW.md for full NF/PC constraint logic.
When This Skill Activates
- Generating new schedules
- Optimizing existing schedules
- Balancing workload distribution
- Resolving scheduling conflicts
- Improving coverage patterns
- Reducing schedule fragmentation
Optimization Objectives
Primary Objectives (Hard Constraints)
These MUST be satisfied - schedule is invalid without them:
| Constraint | Description | Priority |
|---|---|---|
| ACGME Compliance | 80-hour, 1-in-7, supervision | P0 |
| Qualification Match | Only assign qualified personnel | P0 |
| No Double-Booking | One person, one place at a time | P0 |
| Minimum Coverage | Required staffing levels met | P0 |
Secondary Objectives (Soft Constraints)
Optimize these after hard constraints satisfied:
| Objective | Description | Weight |
|---|---|---|
| Fairness | Even workload distribution | 0.25 |
| Preferences | Honor stated preferences | 0.20 |
| Continuity | Minimize handoffs | 0.20 |
| Efficiency | Minimize gaps/fragments | 0.15 |
| Resilience | Maintain backup capacity | 0.20 |
Solver Architecture
Google OR-Tools CP-SAT
Primary constraint programming solver:
# Located in: backend/app/scheduling/engine.py
from ortools.sat.python import cp_model
model = cp_model.CpModel()
# Define variables, constraints, objectives
solver = cp_model.CpSolver()
status = solver.Solve(model)
Solver Configuration
| Parameter | Default | Description |
|---|---|---|
max_time_seconds |
300 | Solver timeout |
num_workers |
8 | Parallel threads |
log_search_progress |
True | Show progress |
Optimization Strategies
1. Pareto Optimization
Find solutions that balance multiple objectives:
No single "best" solution - instead find Pareto frontier:
- Solution A: Best fairness, moderate efficiency
- Solution B: Best efficiency, moderate fairness
- Solution C: Balanced trade-off
MCP Tool:
Tool: generate_pareto_schedules
Input: { objectives: [...], constraints: [...] }
Output: { frontier: [solution1, solution2, ...] }
2. Iterative Improvement
Start with feasible solution, improve incrementally:
1. Generate any valid schedule
2. Identify worst metric
3. Local search for improvements
4. Repeat until no improvement or timeout
3. Decomposition
Break large problem into smaller sub-problems:
Full Year Schedule
├── Q1 (Jan-Mar)
│ ├── Month 1
│ │ ├── Week 1-2
│ │ └── Week 3-4
│ └── ...
└── Q2-Q4 (similar)
Coverage Optimization
Target Coverage Levels
| Rotation | Minimum | Target | Maximum |
|---|---|---|---|
| Inpatient | 2 | 3 | 4 |
| Emergency | 3 | 4 | 5 |
| Clinic | 1 | 2 | 3 |
| Procedures | 1 | 2 | 2 |
Coverage Gap Resolution
Step 1: Identify Gap
SELECT date, session, rotation, COUNT(*) as coverage
FROM assignments
WHERE date BETWEEN :start AND :end
GROUP BY date, session, rotation
HAVING COUNT(*) < minimum_coverage;
Step 2: Find Candidates
- Available personnel (not scheduled)
- Under hour limits
- Qualified for rotation
- Fair workload consideration
Step 3: Assign and Validate
- Make assignment
- Re-run compliance check
- Update metrics
Workload Balancing
Fairness Metrics
| Metric | Formula | Target |
|---|---|---|
| Gini Coefficient | Distribution equality | < 0.15 |
| Std Dev Hours | σ of weekly hours | < 5 |
| Max/Min Ratio | Highest/Lowest load | < 1.3 |
Balancing Algorithm
def balance_workload(assignments):
while gini_coefficient(assignments) > 0.15:
overloaded = find_highest_load()
underloaded = find_lowest_load()
# Find swappable shift
shift = find_transferable_shift(overloaded, underloaded)
if shift and is_valid_transfer(shift):
transfer(shift, from=overloaded, to=underloaded)
else:
break # No valid transfers available
Preference Handling
Preference Types
| Type | Priority | Example |
|---|---|---|
| Hard Block | Highest | "Cannot work Dec 25" |
| Soft Preference | Medium | "Prefer AM shifts" |
| Historical Pattern | Low | Past scheduling data |
Preference Satisfaction
Aim for:
- 100% hard blocks honored
- 80%+ soft preferences
- 70%+ historical patterns
Resilience Integration
80% Utilization Rule
Never schedule above 80% capacity (queuing theory):
If utilization > 80%:
- Queue delays grow exponentially
- No buffer for emergencies
- Burnout risk increases
N-1 Contingency
Schedule must remain valid if any one person unavailable:
Tool: run_contingency_analysis_resilience_tool
Check: Remove each person, verify coverage holds
Static Fallbacks
Pre-compute backup schedules for common failure scenarios:
Tool: get_static_fallbacks_tool
Returns: { scenario: backup_schedule, ... }
Optimization Workflow
New Schedule Generation
Step 1: Gather Inputs
inputs:
- personnel: All available faculty/residents
- rotations: Required rotation coverage
- preferences: Submitted preferences
- constraints: ACGME + program rules
- horizon: Date range to schedule
Step 2: Initialize Solver
engine = SchedulingEngine(
solver="or-tools",
objectives=["compliance", "fairness", "preferences"],
timeout=300
)
Step 3: Generate Solutions
solutions = engine.solve(inputs)
# Returns Pareto frontier of valid schedules
Step 4: Present Options Show decision-makers 3-5 options with trade-offs:
- Option A: Maximizes fairness
- Option B: Maximizes preferences
- Option C: Balanced approach
Step 5: Select and Finalize
- Human selects preferred option
- System validates one more time
- Publish to calendar system
Existing Schedule Optimization
Step 1: Analyze Current State
Tool: analyze_schedule_health
Returns: {
compliance_score,
fairness_score,
coverage_gaps,
improvement_opportunities
}
Step 2: Identify Improvements Rank opportunities by impact/effort:
- Quick wins: Single swap fixes issue
- Medium effort: Multi-swap optimization
- Major restructure: Requires re-solve
Step 3: Apply Changes
- Execute as atomic transaction
- Validate after each change
- Rollback if validation fails
Common Scenarios
Scenario: New Block Schedule
Input: Need 13-week rotation schedule Process:
- Load rotation templates
- Apply qualification constraints
- Balance across 13 weeks
- Optimize for preferences
- Validate ACGME compliance
- Generate 3 options for review
Scenario: Coverage Emergency
Input: 3 faculty out sick tomorrow Process:
- Identify critical gaps
- Query backup pool
- Optimize minimal disruption
- Execute emergency swaps
- Document and rebalance later
Scenario: Fairness Complaint
Input: Resident claims unfair workload Process:
- Run fairness analysis
- Compare to cohort
- If valid, identify rebalancing swaps
- Execute approved changes
- Monitor going forward
Performance Metrics
Solver Performance
| Metric | Target | Action if Missed |
|---|---|---|
| Solve Time | < 5 min | Increase timeout or decompose |
| Solution Quality | > 90% optimal | Tune weights |
| Constraint Satisfaction | 100% hard | Debug constraints |
Schedule Quality
| Metric | Target | Measurement |
|---|---|---|
| ACGME Compliance | 100% | Zero violations |
| Coverage | 100% | All slots filled |
| Fairness (Gini) | < 0.15 | Weekly calculation |
| Preference Match | > 80% | Survey feedback |
MCP Tools Reference
| Tool | Purpose |
|---|---|
generate_schedule |
Create new schedule |
optimize_schedule |
Improve existing schedule |
analyze_schedule_health |
Quality metrics |
generate_pareto_schedules |
Multi-objective options |
validate_schedule |
Compliance check |
run_contingency_analysis_resilience_tool |
N-1/N-2 analysis |
REQUIRED: Documentation After Each Step
Every scheduling task MUST include documentation updates. This prevents knowledge loss between sessions and ensures issues are tracked properly.
Documentation Checkpoint Protocol
After EACH significant step, document:
- What was attempted - The specific action or fix tried
- What happened - Actual results (success, failure, unexpected behavior)
- What was learned - New understanding of the system
- What needs to happen next - Remaining work or blockers
Where to Document
| Finding Type | Location | Example |
|---|---|---|
| Bug/Known Issue | solvers.py header |
Template selection bug |
| Architecture insight | This skill file | Block vs half-day modes |
| Workaround | Code comments + skill | Manual adjustment needed |
| Fix needed | TODO in code + HUMAN_TODO.md | Template filtering |
Planning Template
When starting a scheduling task, create a plan that includes documentation:
## Task: [Description]
### Phase 1: Investigation
- [ ] Explore current state
- [ ] Document findings in [location]
### Phase 2: Implementation
- [ ] Make changes
- [ ] Document what changed in commit message
### Phase 3: Verification
- [ ] Test the changes
- [ ] Document results (success/failure)
### Phase 4: Documentation Update
- [ ] Update skill if new knowledge gained
- [ ] Update code comments if behavior clarified
- [ ] Update HUMAN_TODO.md if manual work needed
Anti-Pattern: Silent Failures
DO NOT:
- Discover an issue and only mention it in chat
- Switch to a "workaround" without documenting why
- Assume the next session will remember context
DO:
- Add issues to code headers immediately
- Update skill files with architectural insights
- Create explicit TODOs for unfixed problems