1use std::collections::HashMap;
10
11use rand::Rng;
12use u_metaheur::ga::GaProblem;
13
14use super::chromosome::ScheduleChromosome;
15use super::operators::GeneticOperators;
16use crate::models::{Assignment, Resource, Schedule, Task, TransitionMatrixCollection};
17
18#[derive(Debug, Clone)]
22pub struct ActivityInfo {
23 pub task_id: String,
25 pub sequence: i32,
27 pub process_ms: i64,
29 pub candidates: Vec<String>,
31}
32
33impl ActivityInfo {
34 pub fn from_tasks(tasks: &[Task]) -> Vec<Self> {
36 let mut infos = Vec::new();
37 for task in tasks {
38 for (i, activity) in task.activities.iter().enumerate() {
39 infos.push(ActivityInfo {
40 task_id: task.id.clone(),
41 sequence: (i + 1) as i32,
42 process_ms: activity.duration.process_ms,
43 candidates: activity
44 .candidate_resources()
45 .into_iter()
46 .map(|s| s.to_string())
47 .collect(),
48 });
49 }
50 }
51 infos
52 }
53}
54
55pub struct SchedulingGaProblem {
72 pub activities: Vec<ActivityInfo>,
74 pub resources: Vec<Resource>,
76 pub task_categories: HashMap<String, String>,
78 pub transition_matrices: TransitionMatrixCollection,
80 pub deadlines: HashMap<String, i64>,
82 pub release_times: HashMap<String, i64>,
84 pub tardiness_weight: f64,
86 pub process_times: HashMap<(String, i32, String), i64>,
91 pub operators: GeneticOperators,
96 activity_index: HashMap<(String, i32), usize>,
100}
101
102impl SchedulingGaProblem {
103 pub fn new(tasks: &[Task], resources: &[Resource]) -> Self {
105 let activities = ActivityInfo::from_tasks(tasks);
106 let mut task_categories = HashMap::new();
107 let mut deadlines = HashMap::new();
108 let mut release_times = HashMap::new();
109
110 for task in tasks {
111 task_categories.insert(task.id.clone(), task.category.clone());
112 if let Some(dl) = task.deadline {
113 deadlines.insert(task.id.clone(), dl);
114 }
115 if let Some(rt) = task.release_time {
116 release_times.insert(task.id.clone(), rt);
117 }
118 }
119
120 let activity_index: HashMap<(String, i32), usize> = activities
122 .iter()
123 .enumerate()
124 .map(|(i, a)| ((a.task_id.clone(), a.sequence), i))
125 .collect();
126
127 Self {
128 activities,
129 resources: resources.to_vec(),
130 task_categories,
131 transition_matrices: TransitionMatrixCollection::new(),
132 deadlines,
133 release_times,
134 tardiness_weight: 0.5,
135 process_times: HashMap::new(),
136 operators: GeneticOperators::default(),
137 activity_index,
138 }
139 }
140
141 pub fn with_transition_matrices(mut self, matrices: TransitionMatrixCollection) -> Self {
143 self.transition_matrices = matrices;
144 self
145 }
146
147 pub fn with_tardiness_weight(mut self, weight: f64) -> Self {
149 self.tardiness_weight = weight.clamp(0.0, 1.0);
150 self
151 }
152
153 pub fn with_process_times(
158 mut self,
159 process_times: HashMap<(String, i32, String), i64>,
160 ) -> Self {
161 self.process_times = process_times;
162 self
163 }
164
165 pub fn with_operators(mut self, operators: GeneticOperators) -> Self {
180 self.operators = operators;
181 self
182 }
183
184 pub fn decode(&self, chromosome: &ScheduleChromosome) -> Schedule {
186 let mut schedule = Schedule::new();
187 let mut resource_available: HashMap<&str, i64> = HashMap::new();
188 let mut task_available: HashMap<&str, i64> = HashMap::new();
189 let mut last_category: HashMap<&str, &str> = HashMap::new();
190
191 for resource in &self.resources {
193 resource_available.insert(&resource.id, 0);
194 }
195
196 let operation_order = chromosome.decode_osv();
198
199 for (task_id, seq) in &operation_order {
200 let act = match self.activity_index.get(&(task_id.clone(), *seq)) {
202 Some(&idx) => &self.activities[idx],
203 None => continue,
204 };
205
206 let resource_id = match chromosome.resource_for(task_id, *seq) {
208 Some(r) if !r.is_empty() => r,
209 _ => continue,
210 };
211
212 let resource_ready = resource_available.get(resource_id).copied().unwrap_or(0);
214 let task_ready = task_available.get(task_id.as_str()).copied().unwrap_or(0);
215 let release = self.release_times.get(task_id).copied().unwrap_or(0);
216 let earliest = resource_ready.max(task_ready).max(release);
217
218 let setup = if let Some(&prev_cat) = last_category.get(resource_id) {
220 let task_cat = self
221 .task_categories
222 .get(task_id)
223 .map(|s| s.as_str())
224 .unwrap_or("");
225 self.transition_matrices
226 .get_transition_time(resource_id, prev_cat, task_cat)
227 } else {
228 0
229 };
230
231 let start = earliest;
232 let end = start + setup + act.process_ms;
233
234 schedule.add_assignment(
235 Assignment::new(&act.task_id, task_id, resource_id, start, end).with_setup(setup),
236 );
237
238 resource_available.insert(resource_id, end);
240 task_available.insert(task_id, end);
241 if let Some(cat) = self.task_categories.get(task_id) {
242 last_category.insert(resource_id, cat);
243 }
244 }
245
246 schedule
247 }
248
249 fn compute_fitness(&self, schedule: &Schedule) -> f64 {
251 let makespan = schedule.makespan_ms() as f64;
252
253 let total_tardiness: f64 = self
254 .deadlines
255 .iter()
256 .map(|(task_id, &deadline)| {
257 let completion = schedule.task_completion_time(task_id).unwrap_or(0);
258 (completion - deadline).max(0) as f64
259 })
260 .sum();
261
262 let makespan_weight = 1.0 - self.tardiness_weight;
264 makespan_weight * makespan + self.tardiness_weight * total_tardiness
265 }
266}
267
268impl GaProblem for SchedulingGaProblem {
269 type Individual = ScheduleChromosome;
270
271 fn create_individual<R: Rng>(&self, rng: &mut R) -> ScheduleChromosome {
272 let roll: f64 = rng.random_range(0.0..1.0);
274 if roll < 0.5 {
275 ScheduleChromosome::random(&self.activities, rng)
276 } else if roll < 0.75 || self.process_times.is_empty() {
277 let cap: HashMap<String, i64> = self
278 .resources
279 .iter()
280 .map(|r| (r.id.clone(), r.capacity as i64))
281 .collect();
282 ScheduleChromosome::with_load_balancing(&self.activities, &cap, rng)
283 } else {
284 ScheduleChromosome::with_shortest_time(&self.activities, &self.process_times, rng)
285 }
286 }
287
288 fn evaluate(&self, individual: &ScheduleChromosome) -> f64 {
289 let schedule = self.decode(individual);
290 self.compute_fitness(&schedule)
291 }
292
293 fn crossover<R: Rng>(
294 &self,
295 parent1: &ScheduleChromosome,
296 parent2: &ScheduleChromosome,
297 rng: &mut R,
298 ) -> Vec<ScheduleChromosome> {
299 let (c1, c2) = self
300 .operators
301 .crossover(parent1, parent2, &self.activities, rng);
302 vec![c1, c2]
303 }
304
305 fn mutate<R: Rng>(&self, individual: &mut ScheduleChromosome, rng: &mut R) {
306 self.operators.mutate(individual, &self.activities, rng);
307 }
308}
309
310unsafe impl Send for SchedulingGaProblem {}
312unsafe impl Sync for SchedulingGaProblem {}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::ga::operators::{CrossoverType, MutationType};
318 use crate::models::{Activity, ActivityDuration, ResourceRequirement, ResourceType};
319 use rand::rngs::SmallRng;
320 use rand::SeedableRng;
321 use u_metaheur::ga::{GaConfig, GaRunner};
322
323 fn make_test_problem() -> (Vec<Task>, Vec<Resource>) {
324 let tasks = vec![
325 Task::new("T1")
326 .with_category("TypeA")
327 .with_priority(5)
328 .with_deadline(10_000)
329 .with_activity(
330 Activity::new("T1_O1", "T1", 0)
331 .with_duration(ActivityDuration::fixed(1000))
332 .with_requirement(
333 ResourceRequirement::new("Machine")
334 .with_candidates(vec!["M1".into(), "M2".into()]),
335 ),
336 )
337 .with_activity(
338 Activity::new("T1_O2", "T1", 1)
339 .with_duration(ActivityDuration::fixed(2000))
340 .with_requirement(
341 ResourceRequirement::new("Machine").with_candidates(vec!["M2".into()]),
342 ),
343 ),
344 Task::new("T2")
345 .with_category("TypeB")
346 .with_priority(3)
347 .with_activity(
348 Activity::new("T2_O1", "T2", 0)
349 .with_duration(ActivityDuration::fixed(1500))
350 .with_requirement(
351 ResourceRequirement::new("Machine")
352 .with_candidates(vec!["M1".into(), "M3".into()]),
353 ),
354 ),
355 ];
356
357 let resources = vec![
358 Resource::new("M1", ResourceType::Primary),
359 Resource::new("M2", ResourceType::Primary),
360 Resource::new("M3", ResourceType::Primary),
361 ];
362
363 (tasks, resources)
364 }
365
366 #[test]
367 fn test_activity_info_from_tasks() {
368 let (tasks, _) = make_test_problem();
369 let infos = ActivityInfo::from_tasks(&tasks);
370 assert_eq!(infos.len(), 3);
371 assert_eq!(infos[0].task_id, "T1");
372 assert_eq!(infos[0].sequence, 1);
373 assert_eq!(infos[0].process_ms, 1000);
374 assert_eq!(infos[2].task_id, "T2");
375 }
376
377 #[test]
378 fn test_decode_chromosome() {
379 let (tasks, resources) = make_test_problem();
380 let problem = SchedulingGaProblem::new(&tasks, &resources);
381 let mut rng = SmallRng::seed_from_u64(42);
382 let ch = problem.create_individual(&mut rng);
383
384 let schedule = problem.decode(&ch);
385 assert!(schedule.assignment_count() > 0);
387 assert!(schedule.makespan_ms() > 0);
388 }
389
390 #[test]
391 fn test_fitness_computation() {
392 let (tasks, resources) = make_test_problem();
393 let problem = SchedulingGaProblem::new(&tasks, &resources);
394 let mut rng = SmallRng::seed_from_u64(42);
395 let ch = problem.create_individual(&mut rng);
396
397 let fitness = problem.evaluate(&ch);
398 assert!(fitness.is_finite());
399 assert!(fitness > 0.0);
400 }
401
402 #[test]
403 fn test_ga_runner_integration() {
404 let (tasks, resources) = make_test_problem();
405 let problem = SchedulingGaProblem::new(&tasks, &resources);
406 let config = GaConfig::default()
407 .with_population_size(20)
408 .with_max_generations(10)
409 .with_seed(42)
410 .with_parallel(false);
411
412 let result = GaRunner::run(&problem, &config).expect("GA should succeed");
413 assert!(result.best_fitness.is_finite());
414 assert!(result.best_fitness < f64::INFINITY);
415 assert!(result.generations > 0);
416 }
417
418 #[test]
419 fn test_crossover_and_mutation() {
420 let (tasks, resources) = make_test_problem();
421 let problem = SchedulingGaProblem::new(&tasks, &resources);
422 let mut rng = SmallRng::seed_from_u64(42);
423
424 let p1 = problem.create_individual(&mut rng);
425 let p2 = problem.create_individual(&mut rng);
426
427 let children = problem.crossover(&p1, &p2, &mut rng);
428 assert_eq!(children.len(), 2);
429
430 let mut child = children[0].clone();
431 problem.mutate(&mut child, &mut rng);
432 assert_eq!(child.osv.len(), p1.osv.len());
433 }
434
435 #[test]
436 fn test_tardiness_weight() {
437 let (tasks, resources) = make_test_problem();
438 let problem_makespan =
439 SchedulingGaProblem::new(&tasks, &resources).with_tardiness_weight(0.0);
440 let problem_tardy = SchedulingGaProblem::new(&tasks, &resources).with_tardiness_weight(1.0);
441
442 let mut rng = SmallRng::seed_from_u64(42);
443 let ch = problem_makespan.create_individual(&mut rng);
444
445 let f1 = problem_makespan.evaluate(&ch);
446 let f2 = problem_tardy.evaluate(&ch);
447 assert!(f1 != f2 || (f1 == 0.0 && f2 == 0.0));
449 }
450
451 #[test]
452 fn test_spt_initialization() {
453 let (tasks, resources) = make_test_problem();
454 let process_times: HashMap<(String, i32, String), i64> = [
455 (("T1".into(), 1, "M1".into()), 500),
456 (("T1".into(), 1, "M2".into()), 900),
457 (("T1".into(), 2, "M2".into()), 2000),
458 (("T2".into(), 1, "M1".into()), 1500),
459 (("T2".into(), 1, "M3".into()), 800),
460 ]
461 .into_iter()
462 .collect();
463
464 let problem =
465 SchedulingGaProblem::new(&tasks, &resources).with_process_times(process_times);
466
467 let mut rng = SmallRng::seed_from_u64(42);
469 for _ in 0..100 {
470 let ch = problem.create_individual(&mut rng);
471 assert_eq!(ch.osv.len(), 3);
472 assert_eq!(ch.mav.len(), 3);
473 }
474 }
475
476 #[test]
477 fn test_with_operators_lox_invert() {
478 let (tasks, resources) = make_test_problem();
479 let ops = GeneticOperators {
480 crossover_type: CrossoverType::LOX,
481 mutation_type: MutationType::Invert,
482 };
483 let problem = SchedulingGaProblem::new(&tasks, &resources).with_operators(ops);
484 let config = GaConfig::default()
485 .with_population_size(20)
486 .with_max_generations(10)
487 .with_seed(42)
488 .with_parallel(false);
489
490 let result = GaRunner::run(&problem, &config).expect("GA should succeed");
491 assert!(result.best_fitness.is_finite());
492 assert!(result.best_fitness < f64::INFINITY);
493 }
494
495 #[test]
496 fn test_with_operators_jox_insert() {
497 let (tasks, resources) = make_test_problem();
498 let ops = GeneticOperators {
499 crossover_type: CrossoverType::JOX,
500 mutation_type: MutationType::Insert,
501 };
502 let problem = SchedulingGaProblem::new(&tasks, &resources).with_operators(ops);
503 let config = GaConfig::default()
504 .with_population_size(20)
505 .with_max_generations(10)
506 .with_seed(99)
507 .with_parallel(false);
508
509 let result = GaRunner::run(&problem, &config).expect("GA should succeed");
510 assert!(result.best_fitness.is_finite());
511 assert!(result.best_fitness < f64::INFINITY);
512 }
513
514 #[test]
515 fn test_default_operators_backward_compatible() {
516 let (tasks, resources) = make_test_problem();
518 let problem = SchedulingGaProblem::new(&tasks, &resources);
519 assert_eq!(problem.operators.crossover_type, CrossoverType::POX);
520 assert_eq!(problem.operators.mutation_type, MutationType::Swap);
521 }
522
523 #[test]
524 fn test_ga_runner_with_process_times() {
525 let (tasks, resources) = make_test_problem();
526 let process_times: HashMap<(String, i32, String), i64> = [
527 (("T1".into(), 1, "M1".into()), 500),
528 (("T1".into(), 1, "M2".into()), 900),
529 (("T1".into(), 2, "M2".into()), 2000),
530 (("T2".into(), 1, "M1".into()), 1500),
531 (("T2".into(), 1, "M3".into()), 800),
532 ]
533 .into_iter()
534 .collect();
535
536 let problem =
537 SchedulingGaProblem::new(&tasks, &resources).with_process_times(process_times);
538 let config = GaConfig::default()
539 .with_population_size(20)
540 .with_max_generations(10)
541 .with_seed(42)
542 .with_parallel(false);
543
544 let result = GaRunner::run(&problem, &config).expect("GA should succeed");
545 assert!(result.best_fitness.is_finite());
546 assert!(result.best_fitness < f64::INFINITY);
547 }
548}