cp running

This commit is contained in:
2025-10-19 20:58:31 +02:00
parent 2976d091cf
commit ce8182244d
6 changed files with 747 additions and 544 deletions

View File

@@ -17,51 +17,43 @@ interface WorkerData {
function buildSchedulingModel(model: CPModel, data: WorkerData): void {
const { employees, shifts, availabilities, constraints } = data;
// 1. Entscheidungsvariablen erstellen
employees.forEach((employee: any) => {
// Filter employees to only include active ones
const activeEmployees = employees.filter(emp => emp.isActive);
const trainees = activeEmployees.filter(emp => emp.employeeType === 'trainee');
const experienced = activeEmployees.filter(emp => emp.employeeType === 'experienced');
console.log(`Building model with ${activeEmployees.length} employees, ${shifts.length} shifts`);
console.log(`Available shifts per week: ${shifts.length}`);
// 1. Create assignment variables for all possible assignments
activeEmployees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const varName = `assign_${employee.id}_${shift.id}`;
model.addVariable(varName, 'bool');
});
});
// 2. Verfügbarkeits-Constraints
employees.forEach((employee: any) => {
// 2. Availability constraints
activeEmployees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const availability = availabilities.find(
(a: any) => a.employeeId === employee.id && a.shiftId === shift.id
);
// Hard constraint: never assign when preference level is 3 (unavailable)
if (availability?.preferenceLevel === 3) {
const varName = `assign_${employee.id}_${shift.id}`;
model.addConstraint(`${varName} == 0`, `Availability constraint for ${employee.name}`);
model.addConstraint(
`${varName} == 0`,
`Hard availability constraint for ${employee.name} in shift ${shift.id}`
);
}
});
});
// 3. Schicht-Besetzungs-Constraints
shifts.forEach((shift: any) => {
const assignmentVars = employees.map(
(emp: any) => `assign_${emp.id}_${shift.id}`
);
if (assignmentVars.length > 0) {
model.addConstraint(
`${assignmentVars.join(' + ')} >= ${shift.minWorkers || 1}`,
`Min workers for shift ${shift.id}`
);
model.addConstraint(
`${assignmentVars.join(' + ')} <= ${shift.maxWorkers || 3}`,
`Max workers for shift ${shift.id}`
);
}
});
// 4. Keine zwei Schichten pro Tag pro Employee
employees.forEach((employee: any) => {
const shiftsByDate = groupShiftsByDate(shifts);
// 3. Max 1 shift per day per employee
const shiftsByDate = groupShiftsByDate(shifts);
activeEmployees.forEach((employee: any) => {
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
const dayAssignmentVars = (dayShifts as any[]).map(
(shift: any) => `assign_${employee.id}_${shift.id}`
@@ -75,79 +67,126 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
}
});
});
// 5. Trainee-Überwachungs-Constraints
const trainees = employees.filter((emp: any) => emp.employeeType === 'trainee');
const experienced = employees.filter((emp: any) => emp.employeeType === 'experienced');
// 4. Shift staffing constraints (RELAXED)
shifts.forEach((shift: any) => {
const assignmentVars = activeEmployees.map(
(emp: any) => `assign_${emp.id}_${shift.id}`
);
if (assignmentVars.length > 0) {
// Minimum workers - make this a soft constraint if possible
const minWorkers = Math.max(shift.minWorkers || 1, 1);
model.addConstraint(
`${assignmentVars.join(' + ')} >= ${minWorkers}`,
`Min workers for shift ${shift.id}`
);
// Maximum workers
const maxWorkers = shift.maxWorkers || 3;
model.addConstraint(
`${assignmentVars.join(' + ')} <= ${maxWorkers}`,
`Max workers for shift ${shift.id}`
);
}
});
// 5. Trainee supervision constraints
trainees.forEach((trainee: any) => {
shifts.forEach((shift: any) => {
const traineeVar = `assign_${trainee.id}_${shift.id}`;
const experiencedVars = experienced.map((exp: any) => `assign_${exp.id}_${shift.id}`);
const experiencedVars = experienced.map((exp: any) =>
`assign_${exp.id}_${shift.id}`
);
if (experiencedVars.length > 0) {
// If trainee works, at least one experienced must work
model.addConstraint(
`${traineeVar} <= ${experiencedVars.join(' + ')}`,
`Trainee ${trainee.name} requires supervision in shift ${shift.id}`
);
} else {
// If no experienced available, trainee cannot work this shift
model.addConstraint(
`${traineeVar} == 0`,
`No experienced staff for trainee ${trainee.name} in shift ${shift.id}`
);
}
});
});
// 6. Contract type constraints
const totalShifts = shifts.length;
console.log(`Total available shifts: ${totalShifts}`);
// 6. Employee cant workalone
employees.forEach((employee: any) => {
if (employee.employeeType === 'experienced' && employee.canWorkAlone) {
shifts.forEach((shift: any) => {
const varName = `assign_${employee.id}_${shift.id}`;
// Allow this employee to work alone (no additional constraint needed)
// This is more about not preventing single assignments
});
activeEmployees.forEach((employee: any) => {
const contractType = employee.contractType || 'large';
// ADJUSTMENT: Make contract constraints feasible with available shifts
let minShifts, maxShifts;
if (contractType === 'small') {
// Small contract: 1 shifts
minShifts = 1;
maxShifts = Math.min(1, totalShifts);
} else {
// Large contract: 2 shifts (2)
minShifts = 2;
maxShifts = Math.min(2, totalShifts);
}
const shiftVars = shifts.map(
(shift: any) => `assign_${employee.id}_${shift.id}`
);
if (shiftVars.length > 0) {
// Use range instead of exact number
model.addConstraint(
`${shiftVars.join(' + ')} == ${minShifts}`,
`Expected shifts for ${employee.name} (${contractType} contract)`
);
model.addConstraint(
`${shiftVars.join(' + ')} <= ${maxShifts}`,
`Max shifts for ${employee.name} (${contractType} contract)`
);
console.log(`Employee ${employee.name}: ${minShifts}-${maxShifts} shifts (${contractType})`);
}
});
// 7. Contract Type Shifts Constraint
employees.forEach((employee: any) => {
const exactShiftsPerWeek = employee.contractType === 'small' ? 5 : 10; // Example: exactly 5 shifts for small, 10 for large
const shiftVars: string[] = [];
// 7. Objective: Maximize preferred assignments with soft constraints
let objectiveExpression = '';
let softConstraintPenalty = '';
activeEmployees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const varName = `assign_${employee.id}_${shift.id}`;
shiftVars.push(varName);
});
if (shiftVars.length > 0) {
model.addConstraint(
`${shiftVars.join(' + ')} == ${exactShiftsPerWeek}`,
`Exact shifts per week for ${employee.name} (${employee.contractType} contract)`
);
}
});
// 8. Ziel: Verfügbarkeits-Score maximieren
let objectiveExpression = '';
employees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const availability = availabilities.find(
(a: any) => a.employeeId === employee.id && a.shiftId === shift.id
);
let score = 0;
if (availability) {
const score = availability.preferenceLevel === 1 ? 10 :
availability.preferenceLevel === 2 ? 5 :
-1000; // Heavy penalty for assigning unavailable shifts
const varName = `assign_${employee.id}_${shift.id}`;
if (objectiveExpression) {
objectiveExpression += ` + ${score} * ${varName}`;
} else {
objectiveExpression = `${score} * ${varName}`;
}
score = availability.preferenceLevel === 1 ? 10 :
availability.preferenceLevel === 2 ? 5 :
-10000; // Very heavy penalty for unavailable
} else {
// No availability info - slight preference to assign
score = 1;
}
if (objectiveExpression) {
objectiveExpression += ` + ${score} * ${varName}`;
} else {
objectiveExpression = `${score} * ${varName}`;
}
});
});
if (objectiveExpression) {
model.maximize(objectiveExpression);
console.log('Objective function set with preference optimization');
}
}
@@ -162,27 +201,94 @@ function groupShiftsByDate(shifts: any[]): Record<string, any[]> {
function extractAssignmentsFromSolution(solution: any, employees: any[], shifts: any[]): any {
const assignments: any = {};
const employeeAssignments: any = {};
// Initialize assignments object with shift IDs
console.log('=== SOLUTION DEBUG INFO ===');
console.log('Solution success:', solution.success);
console.log('Raw assignments from Python:', solution.assignments?.length || 0);
console.log('Variables in solution:', Object.keys(solution.variables || {}).length);
// Initialize assignments object
shifts.forEach((shift: any) => {
assignments[shift.id] = [];
});
// Extract assignments from solution variables
employees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const varName = `assign_${employee.id}_${shift.id}`;
const isAssigned = solution.variables?.[varName] === 1;
if (isAssigned) {
if (!assignments[shift.id]) {
assignments[shift.id] = [];
employeeAssignments[employee.id] = 0;
});
// METHOD 1: Try to use raw variables from solution
if (solution.variables) {
console.log('Using variable-based assignment extraction');
Object.entries(solution.variables).forEach(([varName, value]) => {
if (varName.startsWith('assign_') && value === 1) {
const parts = varName.split('_');
if (parts.length >= 3) {
const employeeId = parts[1];
const shiftId = parts.slice(2).join('_');
// Find the actual shift ID (handle generated IDs)
const actualShift = shifts.find(s =>
s.id === shiftId ||
`assign_${employeeId}_${s.id}` === varName
);
if (actualShift) {
if (!assignments[actualShift.id]) {
assignments[actualShift.id] = [];
}
assignments[actualShift.id].push(employeeId);
employeeAssignments[employeeId]++;
}
}
assignments[shift.id].push(employee.id);
}
});
}
// METHOD 2: Fallback to parsed assignments from Python
if (solution.assignments && solution.assignments.length > 0) {
console.log('Using Python-parsed assignments');
solution.assignments.forEach((assignment: any) => {
const shiftId = assignment.shiftId;
const employeeId = assignment.employeeId;
if (shiftId && employeeId) {
if (!assignments[shiftId]) {
assignments[shiftId] = [];
}
assignments[shiftId].push(employeeId);
employeeAssignments[employeeId]++;
}
});
}
// METHOD 3: Debug - log all variables to see what's available
if (Object.keys(assignments).length === 0 && solution.variables) {
console.log('Debug: First 10 variables from solution:');
const varNames = Object.keys(solution.variables).slice(0, 10);
varNames.forEach(varName => {
console.log(` ${varName} = ${solution.variables[varName]}`);
});
}
// Log results
console.log('=== ASSIGNMENT RESULTS ===');
employees.forEach((employee: any) => {
console.log(` ${employee.name}: ${employeeAssignments[employee.id]} shifts`);
});
let totalAssignments = 0;
shifts.forEach((shift: any) => {
const count = assignments[shift.id]?.length || 0;
totalAssignments += count;
console.log(` Shift ${shift.id}: ${count} employees`);
});
console.log(`Total assignments: ${totalAssignments}`);
console.log('==========================');
return assignments;
}
@@ -244,7 +350,6 @@ async function runScheduling() {
try {
console.log('Starting scheduling optimization...');
// Validate input data
if (!data.shifts || data.shifts.length === 0) {
throw new Error('No shifts provided for scheduling');
@@ -285,16 +390,12 @@ async function runScheduling() {
// Extract assignments from solution
assignments = extractAssignmentsFromSolution(solution, data.employees, data.shifts);
// Detect violations
violations = detectViolations(assignments, data.employees, data.shifts);
if (violations.length === 0) {
resolutionReport.push('✅ No constraint violations detected');
// Only detect violations if we actually have assignments
if (Object.keys(assignments).length > 0) {
violations = detectViolations(assignments, data.employees, data.shifts);
} else {
resolutionReport.push(`⚠️ Found ${violations.length} violations:`);
violations.forEach(violation => {
resolutionReport.push(` - ${violation}`);
});
violations.push('NO_ASSIGNMENTS: Solver reported success but produced no assignments');
console.warn('Solver reported success but produced no assignments. Solution:', solution);
}
// Add assignment statistics