sim_restaurant/
config.rs

1// Copyright (c) 2026 Graphcore Ltd. All rights reserved.
2
3use clap::{Args, Command};
4use gwr_engine::types::SimError;
5
6use crate::time_of_day::TimeOfDay;
7
8#[derive(Debug, Clone, Args)]
9pub struct RestaurantArgs {
10    /// Random seed used to build the day's customer demand.
11    #[arg(long, default_value = "7")]
12    pub seed: u64,
13
14    /// Restaurant opening time in 24-hour format.
15    #[arg(long, default_value = "07:00")]
16    pub opening_time: TimeOfDay,
17
18    /// Restaurant closing time in 24-hour format.
19    #[arg(long, default_value = "22:00")]
20    pub closing_time: TimeOfDay,
21
22    /// Average gap between arrivals during a normal period (in seconds).
23    #[arg(long, default_value = "46")]
24    pub base_arrival_gap: u64,
25
26    /// Random jitter applied to each arrival gap (in seconds).
27    #[arg(long, default_value = "14")]
28    pub arrival_jitter: i64,
29
30    /// Maximum chance that a customer joins when there is no queue.
31    #[arg(long, default_value = "0.98")]
32    pub join_base_probability: f64,
33
34    /// Exponential drop-off in queue joining probability per person in line.
35    #[arg(long, default_value = "0.17")]
36    pub join_queue_sensitivity: f64,
37
38    /// Maximum time a customer will stay in the till queue before leaving (in
39    /// seconds).
40    #[arg(long, default_value = "240")]
41    pub max_queue_wait_ticks: u64,
42
43    /// Time for a customer to walk from the queue to the till (in seconds).
44    #[arg(long, default_value = "6")]
45    pub move_to_till_ticks: u64,
46
47    /// Fixed ordering overhead on top of item-specific times (in seconds).
48    #[arg(long, default_value = "7")]
49    pub order_overhead_ticks: u64,
50
51    /// Time spent paying at the till (in seconds).
52    #[arg(long, default_value = "15")]
53    pub payment_ticks: u64,
54
55    /// Time kitchen staff spend packing order (in seconds).
56    #[arg(long, default_value = "12")]
57    pub pack_order_ticks: u64,
58
59    /// Maximum number of orders allowed to be queued waiting for the kitchen.
60    #[arg(long, default_value = "24")]
61    pub max_kitchen_queue_len: usize,
62
63    /// Time for the customer to collect their food (in seconds).
64    #[arg(long, default_value = "7")]
65    pub take_food_ticks: u64,
66
67    /// Time for the customer to leave after collecting food (in seconds).
68    #[arg(long, default_value = "10")]
69    pub leave_ticks: u64,
70
71    /// Hourly pay cost of one till worker.
72    #[arg(long, default_value = "16.0")]
73    pub till_salary_per_hour: f64,
74
75    /// Hourly pay cost of one kitchen worker.
76    #[arg(long, default_value = "18.0")]
77    pub kitchen_salary_per_hour: f64,
78}
79
80#[must_use]
81pub fn long_arg_name(command: &Command, id: &str) -> String {
82    let long = command
83        .get_arguments()
84        .find(|arg| arg.get_id().as_str() == id)
85        .and_then(|arg| arg.get_long())
86        .map_or_else(|| id.replace('_', "-"), ToOwned::to_owned);
87    format!("--{long}")
88}
89
90#[derive(Debug, Clone, Copy)]
91pub struct RestaurantConfig {
92    pub seed: u64,
93    pub opening_time: TimeOfDay,
94    pub closing_time: TimeOfDay,
95    pub day_ticks: u64,
96    pub base_arrival_gap: u64,
97    pub arrival_jitter: i64,
98    pub join_base_probability: f64,
99    pub join_queue_sensitivity: f64,
100    pub max_queue_wait_ticks: u64,
101    pub move_to_till_ticks: u64,
102    pub order_overhead_ticks: u64,
103    pub payment_ticks: u64,
104    pub pack_order_ticks: u64,
105    pub max_kitchen_queue_len: usize,
106    pub take_food_ticks: u64,
107    pub leave_ticks: u64,
108    pub till_salary_per_hour: f64,
109    pub kitchen_salary_per_hour: f64,
110}
111
112impl From<RestaurantArgs> for RestaurantConfig {
113    fn from(restaurant_args: RestaurantArgs) -> Self {
114        let day_ticks = restaurant_args
115            .closing_time
116            .seconds_since_midnight()
117            .saturating_sub(restaurant_args.opening_time.seconds_since_midnight());
118        Self {
119            seed: restaurant_args.seed,
120            opening_time: restaurant_args.opening_time,
121            closing_time: restaurant_args.closing_time,
122            day_ticks,
123            base_arrival_gap: restaurant_args.base_arrival_gap,
124            arrival_jitter: restaurant_args.arrival_jitter,
125            join_base_probability: restaurant_args.join_base_probability,
126            join_queue_sensitivity: restaurant_args.join_queue_sensitivity,
127            max_queue_wait_ticks: restaurant_args.max_queue_wait_ticks,
128            move_to_till_ticks: restaurant_args.move_to_till_ticks,
129            order_overhead_ticks: restaurant_args.order_overhead_ticks,
130            payment_ticks: restaurant_args.payment_ticks,
131            pack_order_ticks: restaurant_args.pack_order_ticks,
132            max_kitchen_queue_len: restaurant_args.max_kitchen_queue_len,
133            take_food_ticks: restaurant_args.take_food_ticks,
134            leave_ticks: restaurant_args.leave_ticks,
135            till_salary_per_hour: restaurant_args.till_salary_per_hour,
136            kitchen_salary_per_hour: restaurant_args.kitchen_salary_per_hour,
137        }
138    }
139}
140
141impl RestaurantConfig {
142    pub fn validate(&self) -> Result<(), SimError> {
143        let command = RestaurantArgs::augment_args(Command::new("sim-restaurant"));
144        let join_base_probability = long_arg_name(&command, "join_base_probability");
145        let opening_time = long_arg_name(&command, "opening_time");
146        let closing_time = long_arg_name(&command, "closing_time");
147
148        if !(0.0..=1.0).contains(&self.join_base_probability) {
149            return Err(SimError(format!(
150                "`{join_base_probability}` must be in the range 0..=1"
151            )));
152        }
153        if self.opening_time >= self.closing_time {
154            return Err(SimError(format!(
155                "`{opening_time}` must be earlier than `{closing_time}`"
156            )));
157        }
158        if self.day_ticks == 0 {
159            return Err(SimError("day length must be greater than zero".to_string()));
160        }
161        Ok(())
162    }
163
164    #[must_use]
165    pub fn paid_hours(&self) -> u64 {
166        self.day_ticks.div_ceil(3600)
167    }
168}