hydra_engine_wds/simulation/
estimator.rs1use crate::{Network, RuntimeEstimate};
2
3const LOW_EFFORT_THRESHOLD_MS: f64 = 600.0;
4const MEDIUM_EFFORT_THRESHOLD_MS: f64 = 3_000.0;
5
6pub fn estimate_simulation_runtime(network: &Network) -> RuntimeEstimate {
17 estimate_simulation_runtime_from_summary(
18 network.nodes.len(),
19 network.links.len(),
20 network.options.duration,
21 network.options.hyd_step,
22 network.options.qual_step,
23 network.options.quality_mode != crate::QualityMode::None,
24 )
25}
26
27pub fn classify_simulation_runtime_millis(predicted_millis: f64) -> RuntimeEstimate {
29 if predicted_millis < LOW_EFFORT_THRESHOLD_MS {
30 RuntimeEstimate::Low
31 } else if predicted_millis < MEDIUM_EFFORT_THRESHOLD_MS {
32 RuntimeEstimate::Medium
33 } else {
34 RuntimeEstimate::High
35 }
36}
37
38pub fn estimate_simulation_runtime_millis_from_summary(
45 node_count: usize,
46 link_count: usize,
47 duration_seconds: f64,
48 hydraulic_timestep_seconds: f64,
49 quality_timestep_seconds: f64,
50 has_quality: bool,
51) -> f64 {
52 let hydraulic_steps =
53 ((duration_seconds.max(0.0) / hydraulic_timestep_seconds.max(1.0)).floor() + 1.0).max(1.0);
54 let quality_steps =
55 ((duration_seconds.max(0.0) / quality_timestep_seconds.max(1.0)).floor() + 1.0).max(1.0);
56 let nodes = node_count.max(1) as f64;
57 let links = link_count.max(1) as f64;
58
59 let mesh_factor = (links / nodes).clamp(0.75, 2.5);
64 let setup_work_ms = 1.2e-6 * (nodes * nodes) * mesh_factor;
65
66 let hydraulic_step_us = (-2.1 + 0.0266 * (nodes + links)).max(25.0);
69 let hydraulic_work_ms = hydraulic_steps * (hydraulic_step_us / 1_000.0);
70
71 let quality_work_ms = if has_quality {
72 quality_steps * (0.012 * (nodes + links) / 1_000.0)
75 } else {
76 0.0
77 };
78
79 12.0 + setup_work_ms + hydraulic_work_ms + quality_work_ms
80}
81
82pub fn estimate_simulation_runtime_from_summary(
84 node_count: usize,
85 link_count: usize,
86 duration_seconds: f64,
87 hydraulic_timestep_seconds: f64,
88 quality_timestep_seconds: f64,
89 has_quality: bool,
90) -> RuntimeEstimate {
91 classify_simulation_runtime_millis(estimate_simulation_runtime_millis_from_summary(
92 node_count,
93 link_count,
94 duration_seconds,
95 hydraulic_timestep_seconds,
96 quality_timestep_seconds,
97 has_quality,
98 ))
99}
100
101#[cfg(test)]
102mod tests {
103 use super::{
104 classify_simulation_runtime_millis, estimate_simulation_runtime_from_summary,
105 estimate_simulation_runtime_millis_from_summary,
106 };
107
108 #[test]
109 fn estimate_increases_with_network_size() {
110 let small =
111 estimate_simulation_runtime_from_summary(200, 250, 86_400.0, 3_600.0, 300.0, false);
112 let large =
113 estimate_simulation_runtime_from_summary(2_000, 2_500, 86_400.0, 3_600.0, 300.0, false);
114 assert!(large >= small);
115 }
116
117 #[test]
118 fn estimate_increases_with_duration() {
119 let short =
120 estimate_simulation_runtime_from_summary(500, 600, 3_600.0, 3_600.0, 300.0, false);
121 let long =
122 estimate_simulation_runtime_from_summary(500, 600, 86_400.0, 3_600.0, 300.0, false);
123 assert!(long >= short);
124 }
125
126 #[test]
127 fn quality_mode_increases_estimate() {
128 let hyd_only =
129 estimate_simulation_runtime_from_summary(800, 900, 86_400.0, 3_600.0, 300.0, false);
130 let with_quality =
131 estimate_simulation_runtime_from_summary(800, 900, 86_400.0, 3_600.0, 300.0, true);
132 assert!(with_quality >= hyd_only);
133 }
134
135 #[test]
136 fn smaller_quality_timestep_increases_estimate_when_quality_enabled() {
137 let coarse_quality =
138 estimate_simulation_runtime_from_summary(1_200, 1_500, 86_400.0, 3_600.0, 900.0, true);
139 let fine_quality =
140 estimate_simulation_runtime_from_summary(1_200, 1_500, 86_400.0, 3_600.0, 60.0, true);
141 assert!(fine_quality >= coarse_quality);
142 }
143
144 #[test]
145 fn very_large_case_maps_to_high_effort() {
146 let estimate =
147 estimate_simulation_runtime_from_summary(80_000, 90_000, 604_800.0, 300.0, 30.0, true);
148 assert_eq!(estimate, crate::RuntimeEstimate::High);
149 }
150
151 #[test]
152 fn predicted_millis_increase_with_network_size() {
153 let small = estimate_simulation_runtime_millis_from_summary(
154 200, 250, 86_400.0, 3_600.0, 300.0, false,
155 );
156 let large = estimate_simulation_runtime_millis_from_summary(
157 2_000, 2_500, 86_400.0, 3_600.0, 300.0, false,
158 );
159 assert!(large >= small);
160 }
161
162 #[test]
163 fn classifier_thresholds_are_stable() {
164 assert_eq!(
165 classify_simulation_runtime_millis(100.0),
166 crate::RuntimeEstimate::Low
167 );
168 assert_eq!(
169 classify_simulation_runtime_millis(1_000.0),
170 crate::RuntimeEstimate::Medium
171 );
172 assert_eq!(
173 classify_simulation_runtime_millis(10_000.0),
174 crate::RuntimeEstimate::High
175 );
176 }
177}