ferrox/scheduling/
problem.rs1use serde::{Deserialize, Serialize};
2
3use converge_pack::{ExecutionIdentity, FactPayload};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8pub struct SchedulingAgent {
9 pub id: usize,
10 pub name: String,
11 pub capabilities: Vec<String>,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct SchedulingTask {
19 pub id: usize,
20 pub name: String,
21 pub required_capability: String,
23 pub duration_min: i64,
25 pub release_min: i64,
27 pub deadline_min: i64,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct SchedulingRequest {
35 pub id: String,
36 pub agents: Vec<SchedulingAgent>,
37 pub tasks: Vec<SchedulingTask>,
38 pub horizon_min: i64,
40 #[serde(default = "default_time_limit")]
42 pub time_limit_seconds: f64,
43}
44
45impl FactPayload for SchedulingRequest {
46 const FAMILY: &'static str = "ferrox.scheduling.request";
47 const VERSION: u16 = 1;
48}
49
50fn default_time_limit() -> f64 {
51 30.0
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(deny_unknown_fields)]
57pub struct TaskAssignment {
58 pub task_id: usize,
59 pub task_name: String,
60 pub agent_id: usize,
61 pub agent_name: String,
62 pub start_min: i64,
63 pub end_min: i64,
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69pub struct SchedulingPlan {
70 pub request_id: String,
71 pub assignments: Vec<TaskAssignment>,
72 pub tasks_total: usize,
73 pub tasks_scheduled: usize,
74 pub makespan_min: i64,
76 pub solver: String,
78 pub execution_identity: ExecutionIdentity,
79 pub status: String,
81 pub wall_time_seconds: f64,
82}
83
84impl FactPayload for SchedulingPlan {
85 const FAMILY: &'static str = "ferrox.scheduling.plan";
86 const VERSION: u16 = 1;
87}
88
89impl SchedulingPlan {
90 #[allow(clippy::cast_precision_loss)]
92 pub fn throughput_ratio(&self) -> f64 {
93 if self.tasks_total == 0 {
94 return 0.0;
95 }
96 self.tasks_scheduled as f64 / self.tasks_total as f64
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::solver_identity::non_native_solver_identity;
104
105 fn empty_plan(tasks_total: usize, tasks_scheduled: usize) -> SchedulingPlan {
106 SchedulingPlan {
107 request_id: "r".into(),
108 assignments: vec![],
109 tasks_total,
110 tasks_scheduled,
111 makespan_min: 0,
112 solver: "x".into(),
113 execution_identity: non_native_solver_identity("x", "test"),
114 status: "feasible".into(),
115 wall_time_seconds: 0.0,
116 }
117 }
118
119 #[test]
120 fn throughput_ratio_zero_when_no_tasks() {
121 let p = empty_plan(0, 0);
122 assert!((p.throughput_ratio() - 0.0).abs() < f64::EPSILON);
123 }
124
125 #[test]
126 fn throughput_ratio_partial() {
127 let p = empty_plan(10, 7);
128 assert!((p.throughput_ratio() - 0.7).abs() < 1e-9);
129 }
130
131 #[test]
132 fn request_serde_round_trip_with_default_time_limit() {
133 let json = r#"{"id":"r","agents":[],"tasks":[],"horizon_min":120}"#;
134 let r: SchedulingRequest = serde_json::from_str(json).unwrap();
135 assert!((r.time_limit_seconds - 30.0).abs() < f64::EPSILON);
136 }
137}