Skip to main content

ferrox/jobshop/
problem.rs

1use serde::{Deserialize, Serialize};
2
3use converge_pack::{ExecutionIdentity, FactPayload};
4
5/// One operation within a job: must execute on `machine_id` for `duration` units.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8pub struct Operation {
9    pub machine_id: usize,
10    pub duration: i64,
11}
12
13/// A job is an ordered sequence of operations; each must complete before the next begins.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct Job {
17    pub id: usize,
18    pub name: String,
19    /// Operations in their required execution order.
20    pub operations: Vec<Operation>,
21}
22
23/// Seeded into `ContextKey::Seeds` with id prefix `"jspbench-request:"`.
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct JobShopRequest {
27    pub id: String,
28    pub jobs: Vec<Job>,
29    pub num_machines: usize,
30    #[serde(default = "default_time_limit")]
31    pub time_limit_seconds: f64,
32}
33
34impl FactPayload for JobShopRequest {
35    const FAMILY: &'static str = "ferrox.jobshop.request";
36    const VERSION: u16 = 1;
37}
38
39fn default_time_limit() -> f64 {
40    30.0
41}
42
43impl JobShopRequest {
44    /// Trivial upper bound on makespan: sum of all operation durations.
45    pub fn horizon(&self) -> i64 {
46        self.jobs
47            .iter()
48            .flat_map(|j| j.operations.iter())
49            .map(|o| o.duration)
50            .sum()
51    }
52}
53
54/// A scheduled operation with resolved timing.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(deny_unknown_fields)]
57pub struct ScheduledOp {
58    pub job_id: usize,
59    pub job_name: String,
60    pub machine_id: usize,
61    pub op_index: usize,
62    pub start: i64,
63    pub end: i64,
64}
65
66/// Written to `ContextKey::Strategies` with id prefix `"jspbench-plan-<solver>:"`.
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69pub struct JobShopPlan {
70    pub request_id: String,
71    pub schedule: Vec<ScheduledOp>,
72    pub makespan: i64,
73    /// Proven lower bound (available when status is `"optimal"`).
74    pub lower_bound: Option<i64>,
75    pub solver: String,
76    pub execution_identity: ExecutionIdentity,
77    /// `"optimal"`, `"feasible"`, or `"error"`.
78    pub status: String,
79    pub wall_time_seconds: f64,
80}
81
82impl FactPayload for JobShopPlan {
83    const FAMILY: &'static str = "ferrox.jobshop.plan";
84    const VERSION: u16 = 1;
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn horizon_zero_when_no_jobs() {
93        let r = JobShopRequest {
94            id: "r".into(),
95            jobs: vec![],
96            num_machines: 1,
97            time_limit_seconds: 1.0,
98        };
99        assert_eq!(r.horizon(), 0);
100    }
101
102    #[test]
103    fn horizon_sums_durations() {
104        let r = JobShopRequest {
105            id: "r".into(),
106            jobs: vec![Job {
107                id: 0,
108                name: "j".into(),
109                operations: vec![
110                    Operation {
111                        machine_id: 0,
112                        duration: 4,
113                    },
114                    Operation {
115                        machine_id: 1,
116                        duration: 6,
117                    },
118                ],
119            }],
120            num_machines: 2,
121            time_limit_seconds: 1.0,
122        };
123        assert_eq!(r.horizon(), 10);
124    }
125
126    #[test]
127    fn request_default_time_limit() {
128        let json = r#"{"id":"r","jobs":[],"num_machines":1}"#;
129        let r: JobShopRequest = serde_json::from_str(json).unwrap();
130        assert!((r.time_limit_seconds - 30.0).abs() < f64::EPSILON);
131    }
132}