mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
cp running
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
// backend/src/workers/cp-sat-wrapper.ts
|
||||
import { execSync } from 'child_process';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { spawn } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { SolverOptions, Solution, Assignment, Violation } from '../models/scheduling.js';
|
||||
import { SolverOptions, Solution, Assignment } from '../models/scheduling.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -54,51 +53,216 @@ export class CPSolver {
|
||||
constructor(private options: SolverOptions) {}
|
||||
|
||||
async solve(model: CPModel): Promise<Solution> {
|
||||
await this.checkPythonEnvironment();
|
||||
|
||||
try {
|
||||
return await this.solveViaPythonBridge(model);
|
||||
} catch (error) {
|
||||
console.error('CP-SAT bridge failed, falling back to TypeScript solver:', error);
|
||||
console.error('CP-SAT bridge failed, using TypeScript fallback:', error);
|
||||
return await this.solveWithTypeScript(model);
|
||||
}
|
||||
}
|
||||
|
||||
private async solveViaPythonBridge(model: CPModel): Promise<Solution> {
|
||||
const pythonScriptPath = path.resolve(__dirname, '../../python-scripts/scheduling_solver.py');
|
||||
// Try multiple possible paths for the Python script
|
||||
const possiblePaths = [
|
||||
path.resolve(process.cwd(), 'python-scripts/scheduling_solver.py'),
|
||||
path.resolve(process.cwd(), 'backend/python-scripts/scheduling_solver.py'),
|
||||
path.resolve(__dirname, '../../../python-scripts/scheduling_solver.py'),
|
||||
path.resolve(__dirname, '../../src/python-scripts/scheduling_solver.py'),
|
||||
];
|
||||
|
||||
let pythonScriptPath = '';
|
||||
for (const p of possiblePaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
pythonScriptPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pythonScriptPath) {
|
||||
throw new Error(`Python script not found. Tried: ${possiblePaths.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log('Using Python script at:', pythonScriptPath);
|
||||
|
||||
const modelData = model.export();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const pythonProcess = spawn('python', [pythonScriptPath], {
|
||||
timeout: this.options.maxTimeInSeconds * 1000,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
console.error(`Python process exited with code ${code}`);
|
||||
if (stderr) {
|
||||
console.error('Python stderr:', stderr);
|
||||
}
|
||||
reject(new Error(`Python script failed with code ${code}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Python raw output:', stdout.substring(0, 500)); // Debug log
|
||||
|
||||
const result = JSON.parse(stdout);
|
||||
|
||||
// ENHANCED: Better solution parsing
|
||||
const solution: Solution = {
|
||||
success: result.success || false,
|
||||
assignments: result.assignments || [],
|
||||
violations: result.violations || [],
|
||||
metadata: {
|
||||
solveTime: result.metadata?.solveTime || 0,
|
||||
constraintsAdded: result.metadata?.constraintsAdded || 0,
|
||||
variablesCreated: result.metadata?.variablesCreated || 0,
|
||||
optimal: result.metadata?.optimal || false
|
||||
},
|
||||
variables: result.variables || {}
|
||||
};
|
||||
|
||||
console.log(`Python solver result: success=${solution.success}, assignments=${solution.assignments.length}`);
|
||||
|
||||
resolve(solution);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse Python output. Raw output:', stdout.substring(0, 500));
|
||||
reject(new Error(`Invalid JSON from Python: ${parseError}`));
|
||||
}
|
||||
});
|
||||
|
||||
pythonProcess.on('error', (error) => {
|
||||
console.error('Failed to start Python process:', error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// Send input data
|
||||
pythonProcess.stdin.write(JSON.stringify({
|
||||
modelData: modelData,
|
||||
solverOptions: this.options
|
||||
}));
|
||||
pythonProcess.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
private async checkPythonEnvironment(): Promise<boolean> {
|
||||
try {
|
||||
return new Promise((resolve) => {
|
||||
// Try multiple Python commands
|
||||
const commands = ['python', 'python3', 'py'];
|
||||
let currentCommandIndex = 0;
|
||||
|
||||
const tryNextCommand = () => {
|
||||
if (currentCommandIndex >= commands.length) {
|
||||
console.log('❌ Python is not available (tried: ' + commands.join(', ') + ')');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const command = commands[currentCommandIndex];
|
||||
const pythonProcess = spawn(command, ['--version']);
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log(`✅ Python is available (using: ${command})`);
|
||||
resolve(true);
|
||||
} else {
|
||||
currentCommandIndex++;
|
||||
tryNextCommand();
|
||||
}
|
||||
});
|
||||
|
||||
pythonProcess.on('error', () => {
|
||||
currentCommandIndex++;
|
||||
tryNextCommand();
|
||||
});
|
||||
};
|
||||
|
||||
tryNextCommand();
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async solveWithTypeScript(model: CPModel): Promise<Solution> {
|
||||
const startTime = Date.now();
|
||||
const modelData = model.export();
|
||||
|
||||
const result = execSync(`python3 "${pythonScriptPath}"`, {
|
||||
input: JSON.stringify(modelData),
|
||||
timeout: this.options.maxTimeInSeconds * 1000,
|
||||
encoding: 'utf-8',
|
||||
maxBuffer: 50 * 1024 * 1024 // 50MB buffer für große Probleme
|
||||
console.log('Using TypeScript fallback solver');
|
||||
console.log(`Model has ${Object.keys(modelData.variables).length} variables and ${modelData.constraints.length} constraints`);
|
||||
|
||||
// Create a simple feasible solution
|
||||
const assignments: Assignment[] = [];
|
||||
|
||||
// Generate basic assignments - try to satisfy constraints
|
||||
const employeeShiftCount: {[key: string]: number} = {};
|
||||
const shiftAssignments: {[key: string]: string[]} = {};
|
||||
|
||||
// Initialize
|
||||
Object.keys(modelData.variables).forEach(varName => {
|
||||
if (varName.startsWith('assign_')) {
|
||||
const parts = varName.split('_');
|
||||
if (parts.length >= 3) {
|
||||
const employeeId = parts[1];
|
||||
const shiftId = parts.slice(2).join('_');
|
||||
|
||||
if (!employeeShiftCount[employeeId]) employeeShiftCount[employeeId] = 0;
|
||||
if (!shiftAssignments[shiftId]) shiftAssignments[shiftId] = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
private async solveWithTypeScript(model: CPModel): Promise<Solution> {
|
||||
// Einfacher TypeScript CSP Solver als Fallback
|
||||
return this.basicBacktrackingSolver(model);
|
||||
}
|
||||
|
||||
private async basicBacktrackingSolver(model: CPModel): Promise<Solution> {
|
||||
// Einfache Backtracking-Implementierung
|
||||
// Für kleine Probleme geeignet
|
||||
const startTime = Date.now();
|
||||
// Simple assignment logic
|
||||
Object.keys(modelData.variables).forEach(varName => {
|
||||
if (modelData.variables[varName].type === 'bool' && varName.startsWith('assign_')) {
|
||||
const parts = varName.split('_');
|
||||
if (parts.length >= 3) {
|
||||
const employeeId = parts[1];
|
||||
const shiftId = parts.slice(2).join('_');
|
||||
|
||||
// Simple logic: assign about 30% of shifts randomly, but respect some constraints
|
||||
const shouldAssign = Math.random() > 0.7 && employeeShiftCount[employeeId] < 10;
|
||||
|
||||
if (shouldAssign) {
|
||||
assignments.push({
|
||||
shiftId,
|
||||
employeeId,
|
||||
assignedAt: new Date(),
|
||||
score: Math.floor(Math.random() * 50) + 50 // Random score 50-100
|
||||
});
|
||||
employeeShiftCount[employeeId]++;
|
||||
shiftAssignments[shiftId].push(employeeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Hier einfache CSP-Logik implementieren
|
||||
const assignments: Assignment[] = [];
|
||||
const violations: Violation[] = [];
|
||||
const processingTime = Date.now() - startTime;
|
||||
|
||||
console.log(`TypeScript solver created ${assignments.length} assignments in ${processingTime}ms`);
|
||||
|
||||
return {
|
||||
assignments,
|
||||
violations,
|
||||
success: violations.length === 0,
|
||||
violations: [],
|
||||
success: assignments.length > 0,
|
||||
metadata: {
|
||||
solveTime: Date.now() - startTime,
|
||||
constraintsAdded: model.export().constraints.length,
|
||||
variablesCreated: Object.keys(model.export().variables).length,
|
||||
optimal: true
|
||||
solveTime: processingTime,
|
||||
constraintsAdded: modelData.constraints.length,
|
||||
variablesCreated: Object.keys(modelData.variables).length,
|
||||
optimal: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user