1use serde::{Deserialize, Serialize};
2
3use converge_pack::{ExecutionIdentity, FactPayload};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum VrptwSolveStatus {
8 Optimal,
9 Feasible,
10 Infeasible,
11 Error,
12 Invalid,
13}
14
15impl VrptwSolveStatus {
16 pub fn is_successful(self) -> bool {
17 matches!(self, Self::Optimal | Self::Feasible)
18 }
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct Customer {
25 pub id: usize,
26 pub name: String,
27 pub x: f64,
28 pub y: f64,
29 pub window_open: i64,
31 pub window_close: i64,
33 pub service_time: i64,
35}
36
37impl Customer {
38 pub fn travel_to(&self, other: &Customer) -> f64 {
39 let dx = self.x - other.x;
40 let dy = self.y - other.y;
41 (dx * dx + dy * dy).sqrt()
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct Depot {
49 pub x: f64,
50 pub y: f64,
51 pub ready_time: i64,
52 pub due_time: i64,
53}
54
55impl Depot {
56 pub fn travel_to_customer(&self, c: &Customer) -> f64 {
57 let dx = self.x - c.x;
58 let dy = self.y - c.y;
59 (dx * dx + dy * dy).sqrt()
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct VrptwRequest {
71 pub id: String,
72 pub depot: Depot,
73 pub customers: Vec<Customer>,
74 #[serde(default = "default_time_limit")]
75 pub time_limit_seconds: f64,
76}
77
78impl FactPayload for VrptwRequest {
79 const FAMILY: &'static str = "ferrox.vrptw.request";
80 const VERSION: u16 = 1;
81}
82
83fn default_time_limit() -> f64 {
84 30.0
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(deny_unknown_fields)]
90pub struct RouteStop {
91 pub customer_id: usize,
92 pub customer_name: String,
93 pub arrival: i64,
94 pub departure: i64,
95}
96
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct VrptwPlan {
101 pub request_id: String,
102 pub route: Vec<RouteStop>,
104 pub customers_total: usize,
105 pub customers_visited: usize,
106 pub total_distance: f64,
108 pub return_time: i64,
110 pub solver: String,
111 pub execution_identity: ExecutionIdentity,
112 pub status: VrptwSolveStatus,
113 pub wall_time_seconds: f64,
114}
115
116impl FactPayload for VrptwPlan {
117 const FAMILY: &'static str = "ferrox.vrptw.plan";
118 const VERSION: u16 = 1;
119}
120
121impl VrptwPlan {
122 #[allow(clippy::cast_precision_loss)]
123 pub fn visit_ratio(&self) -> f64 {
124 if self.customers_total == 0 {
125 return 0.0;
126 }
127 self.customers_visited as f64 / self.customers_total as f64
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::solver_identity::non_native_solver_identity;
135
136 fn cust(x: f64, y: f64) -> Customer {
137 Customer {
138 id: 1,
139 name: "c".into(),
140 x,
141 y,
142 window_open: 0,
143 window_close: 100,
144 service_time: 1,
145 }
146 }
147
148 #[test]
149 fn customer_travel_is_euclidean() {
150 let a = cust(0.0, 0.0);
151 let b = cust(3.0, 4.0);
152 assert!((a.travel_to(&b) - 5.0).abs() < 1e-9);
153 }
154
155 #[test]
156 fn depot_travel_to_customer_is_euclidean() {
157 let d = Depot {
158 x: 0.0,
159 y: 0.0,
160 ready_time: 0,
161 due_time: 100,
162 };
163 assert!((d.travel_to_customer(&cust(0.0, 5.0)) - 5.0).abs() < 1e-9);
164 }
165
166 #[test]
167 fn visit_ratio_zero_when_no_customers() {
168 let p = VrptwPlan {
169 request_id: "r".into(),
170 route: vec![],
171 customers_total: 0,
172 customers_visited: 0,
173 total_distance: 0.0,
174 return_time: 0,
175 solver: "x".into(),
176 execution_identity: non_native_solver_identity("x", "test"),
177 status: VrptwSolveStatus::Feasible,
178 wall_time_seconds: 0.0,
179 };
180 assert!((p.visit_ratio() - 0.0).abs() < f64::EPSILON);
181 }
182
183 #[test]
184 fn request_default_time_limit() {
185 let json =
186 r#"{"id":"r","depot":{"x":0,"y":0,"ready_time":0,"due_time":100},"customers":[]}"#;
187 let r: VrptwRequest = serde_json::from_str(json).unwrap();
188 assert!((r.time_limit_seconds - 30.0).abs() < f64::EPSILON);
189 }
190}