ferrox/scheduling/
problem.rs1use serde::{Deserialize, Serialize};
2
3use converge_pack::{ExecutionIdentity, FactPayload};
4
5use crate::domain_types::{AgentId, Minutes, TaskId};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum SchedulingSolveStatus {
10 Optimal,
11 Feasible,
12 Infeasible,
13 Unbounded,
14 Error,
15 Invalid,
16}
17
18impl SchedulingSolveStatus {
19 pub fn is_successful(self) -> bool {
20 matches!(self, Self::Optimal | Self::Feasible)
21 }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct SchedulingAgent {
28 pub id: AgentId,
29 pub name: String,
30 pub capabilities: Vec<String>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct SchedulingTask {
38 pub id: TaskId,
39 pub name: String,
40 pub required_capability: String,
42 pub duration_min: Minutes,
44 pub release_min: Minutes,
46 pub deadline_min: Minutes,
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct SchedulingRequest {
54 pub id: String,
55 pub agents: Vec<SchedulingAgent>,
56 pub tasks: Vec<SchedulingTask>,
57 pub horizon_min: i64,
59 #[serde(default = "default_time_limit")]
61 pub time_limit_seconds: f64,
62}
63
64impl FactPayload for SchedulingRequest {
65 const FAMILY: &'static str = "ferrox.scheduling.request";
66 const VERSION: u16 = 1;
67}
68
69fn default_time_limit() -> f64 {
70 30.0
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct TaskAssignment {
77 pub task_id: TaskId,
78 pub task_name: String,
79 pub agent_id: AgentId,
80 pub agent_name: String,
81 pub start_min: Minutes,
82 pub end_min: Minutes,
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct SchedulingPlan {
89 pub request_id: String,
90 pub assignments: Vec<TaskAssignment>,
91 pub tasks_total: usize,
92 pub tasks_scheduled: usize,
93 pub makespan_min: Minutes,
95 pub solver: String,
97 pub execution_identity: ExecutionIdentity,
98 pub status: SchedulingSolveStatus,
99 pub wall_time_seconds: f64,
100}
101
102impl FactPayload for SchedulingPlan {
103 const FAMILY: &'static str = "ferrox.scheduling.plan";
104 const VERSION: u16 = 1;
105}
106
107impl SchedulingPlan {
108 #[allow(clippy::cast_precision_loss)]
110 pub fn throughput_ratio(&self) -> f64 {
111 if self.tasks_total == 0 {
112 return 0.0;
113 }
114 self.tasks_scheduled as f64 / self.tasks_total as f64
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::domain_types::Minutes;
122 use crate::solver_identity::non_native_solver_identity;
123
124 fn empty_plan(tasks_total: usize, tasks_scheduled: usize) -> SchedulingPlan {
125 SchedulingPlan {
126 request_id: "r".into(),
127 assignments: vec![],
128 tasks_total,
129 tasks_scheduled,
130 makespan_min: Minutes(0),
131 solver: "x".into(),
132 execution_identity: non_native_solver_identity("x", "test"),
133 status: SchedulingSolveStatus::Feasible,
134 wall_time_seconds: 0.0,
135 }
136 }
137
138 #[test]
139 fn throughput_ratio_zero_when_no_tasks() {
140 let p = empty_plan(0, 0);
141 assert!((p.throughput_ratio() - 0.0).abs() < f64::EPSILON);
142 }
143
144 #[test]
145 fn throughput_ratio_partial() {
146 let p = empty_plan(10, 7);
147 assert!((p.throughput_ratio() - 0.7).abs() < 1e-9);
148 }
149
150 #[test]
151 fn request_serde_round_trip_with_default_time_limit() {
152 let json = r#"{"id":"r","agents":[],"tasks":[],"horizon_min":120}"#;
153 let r: SchedulingRequest = serde_json::from_str(json).unwrap();
154 assert!((r.time_limit_seconds - 30.0).abs() < f64::EPSILON);
155 }
156}