File size: 4,944 Bytes
4b94493
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
use solverforge::{ConstraintSet, SolverEvent, SolverManager};
use std::collections::BTreeSet;

use super::{generate, DemoData};
use crate::domain::Plan;

// Static manager — must be 'static for retained job execution.
static MANAGER: SolverManager<Plan> = SolverManager::new();

fn schedule() -> Plan {
    generate(DemoData::Large)
}

/// Slow end-to-end acceptance test for the large dataset.
///
/// This verifies that the solver starts from an unassigned schedule, reaches a
/// hard/medium-feasible timetable, and keeps a visible soft optimization score.
#[test]
#[ignore = "slow acceptance test for the large dataset"]
fn large_demo_solves_to_feasible_progressing_schedule() {
    let plan = schedule();
    let initial_score = crate::constraints::create_constraints().evaluate_all(&plan);
    assert_eq!(
        initial_score,
        solverforge::HardMediumSoftScore::of_medium(-600),
        "The generated demo must start unassigned, not already solved"
    );

    let (job_id, mut receiver) = MANAGER.solve(plan).expect("job should start");
    let mut completed_score = None;
    let mut completed_solution = None;
    let mut observed_scores = Vec::new();

    while let Some(event) = receiver.blocking_recv() {
        if let Some(score) = event.metadata().current_score {
            observed_scores.push(score);
        }

        match event {
            SolverEvent::Completed { solution, .. } => {
                completed_score = solution.score;
                completed_solution = Some(solution);
                break;
            }
            SolverEvent::Failed { error, .. } => {
                panic!("demo solve failed unexpectedly: {error}");
            }
            _ => {}
        }
    }

    let score = completed_score.expect("expected a completed score");
    let solution = completed_solution.expect("expected a completed solution");

    // The best solution must satisfy both the hard feasibility rules and the
    // medium-level assignment requirements while retaining soft optimization
    // pressure that lets the UI show continued score movement.
    assert_eq!(
        score.hard(),
        0,
        "Expected hard-feasible solution, but got: {}",
        score
    );
    assert_eq!(
        score.medium(),
        0,
        "Expected all lessons assigned, but got: {}",
        score
    );
    assert!(
        score.soft() < 0,
        "Expected remaining soft penalties for realistic timetable quality, got: {}",
        score
    );
    assert!(
        score.medium() > initial_score.medium(),
        "Expected terminal score to improve from the unassigned medium penalty"
    );
    assert!(
        observed_scores.contains(&initial_score),
        "Expected event stream to expose the unassigned initial score"
    );
    assert!(
        observed_scores
            .iter()
            .any(|score| score.hard() == 0 && score.medium() == 0 && score.soft() < 0),
        "Expected event stream to expose a feasible soft-scored timetable"
    );

    let lesson_count = solution.lessons.len();
    let assigned_timeslots = solution
        .lessons
        .iter()
        .filter(|l| l.timeslot_idx.is_some())
        .count();
    let assigned_rooms = solution
        .lessons
        .iter()
        .filter(|l| l.room_idx.is_some())
        .count();

    assert_eq!(
        assigned_timeslots, lesson_count,
        "Every lesson must have a timeslot assignment"
    );
    assert_eq!(
        assigned_rooms, lesson_count,
        "Every lesson must have a room assignment"
    );

    let distinct_timeslots = solution
        .lessons
        .iter()
        .filter_map(|lesson| lesson.timeslot_idx)
        .collect::<BTreeSet<_>>()
        .len();
    let distinct_rooms = solution
        .lessons
        .iter()
        .filter_map(|lesson| lesson.room_idx)
        .collect::<BTreeSet<_>>()
        .len();

    assert!(
        distinct_timeslots > 1,
        "The solved timetable must not collapse every lesson into one timeslot"
    );
    assert!(
        distinct_rooms > 1,
        "The solved timetable must not collapse every lesson into one room"
    );

    let constraints = crate::constraints::create_constraints();
    let hard_or_medium_constraints: Vec<_> = constraints
        .evaluate_detailed(&solution)
        .into_iter()
        .filter(|analysis| analysis.score.hard() != 0 || analysis.score.medium() != 0)
        .map(|analysis| format!("{}={}", analysis.constraint_ref.name, analysis.score))
        .collect();
    assert!(
        hard_or_medium_constraints.is_empty(),
        "Expected all hard/medium constraints to score zero, got: {}",
        hard_or_medium_constraints.join(", ")
    );

    eprintln!(
        "Solution: {} lessons, {} timeslots assigned, {} rooms assigned, score {}",
        lesson_count, assigned_timeslots, assigned_rooms, score
    );

    MANAGER.delete(job_id).expect("delete completed job");
}