1use serde::{Deserialize, Serialize};
10
11use crate::{controller_type::ControllerType, process_type::ProcessType};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
17#[serde(rename_all = "snake_case")]
18pub enum ResponseLevel {
19 Aggressive = 0,
20 Moderate = 1,
21 Sluggish = 2,
22}
23
24impl ResponseLevel {
25 pub const ALL: [ResponseLevel; 3] = [
26 ResponseLevel::Aggressive,
27 ResponseLevel::Moderate,
28 ResponseLevel::Sluggish,
29 ];
30
31 fn index(self) -> usize {
32 self as usize
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
39pub struct TuningConstants {
40 pub c1: f32,
44 pub c2: f32,
46 pub c3: f32,
50 pub beta: f32,
53}
54
55pub(crate) const DEFAULT_CYCLES_SKIP: [u32; 6] = [1, 1, 1, 1, 1, 1];
58
59pub(crate) const DEFAULT_CYCLES_TEST: [u32; 6] = [2, 2, 1, 1, 1, 1];
61
62pub(crate) const DEFAULT_NOISE_PROTECTION_SECS: [u32; 6] = [3, 3, 10, 10, 20, 20];
64
65const C1: [[[f32; 3]; 3]; 6] = [
68 [[0.5, 0.451, 0.0], [0.333, 0.302, 0.0], [0.25, 0.226, 0.0]],
70 [[0.5, 0.442, 0.0], [0.333, 0.296, 0.0], [0.25, 0.221, 0.0]],
72 [
74 [0.333, 0.331, 0.0],
75 [0.222, 0.222, 0.0],
76 [0.167, 0.166, 0.0],
77 ],
78 [
80 [0.333, 0.331, 0.0],
81 [0.222, 0.222, 0.0],
82 [0.167, 0.166, 0.0],
83 ],
84 [
86 [0.5, 0.47, 0.498],
87 [0.25, 0.235, 0.249],
88 [0.167, 0.155, 0.164],
89 ],
90 [
92 [0.333, 0.325, 0.332],
93 [0.222, 0.218, 0.222],
94 [0.167, 0.163, 0.166],
95 ],
96];
97
98const C2: [[f32; 3]; 6] = [
102 [0.0, 0.331, 0.0], [0.0, 0.302, 0.0], [0.0, 1.216, 0.0], [0.0, 1.216, 0.0], [0.0, 0.436, 0.162], [0.0, 0.704, 0.275], ];
109
110const C3: [[f32; 3]; 6] = [
113 [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.14], [0.0, 0.0, 0.09], ];
120
121const BETA: [[f32; 3]; 6] = [
124 [0.0, 0.433, 0.0], [0.0, 0.466, 0.0], [0.0, 0.13, 0.0], [0.0, 0.13, 0.0], [0.0, 0.343, 0.102], [0.0, 0.221, 0.013], ];
131
132pub fn lookup(
136 process_type: ProcessType,
137 controller_type: ControllerType,
138 response_level: ResponseLevel,
139) -> TuningConstants {
140 let p = process_type.index();
141 let c = controller_type.index();
142 TuningConstants {
143 c1: C1[p][response_level.index()][c],
144 c2: C2[p][c],
145 c3: C3[p][c],
146 beta: BETA[p][c],
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn flow_pi_aggressive_matches_known_constants() {
156 let tc = lookup(
157 ProcessType::Flow,
158 ControllerType::Pi,
159 ResponseLevel::Aggressive,
160 );
161 assert_eq!(tc.c1, 0.451);
162 assert_eq!(tc.c2, 0.331);
163 assert_eq!(tc.c3, 0.0);
164 assert_eq!(tc.beta, 0.433);
165 }
166
167 #[test]
168 fn flow_pi_moderate_and_sluggish_differ_only_in_c1() {
169 let moderate = lookup(
170 ProcessType::Flow,
171 ControllerType::Pi,
172 ResponseLevel::Moderate,
173 );
174 let sluggish = lookup(
175 ProcessType::Flow,
176 ControllerType::Pi,
177 ResponseLevel::Sluggish,
178 );
179 assert_eq!(moderate.c1, 0.302);
180 assert_eq!(sluggish.c1, 0.226);
181 assert_eq!(moderate.c2, sluggish.c2);
182 assert_eq!(moderate.c3, sluggish.c3);
183 assert_eq!(moderate.beta, sluggish.beta);
184 }
185
186 #[test]
190 fn p_only_controller_has_zero_beta_c2_c3_but_nonzero_c1() {
191 for pt in ProcessType::ALL {
192 for response in ResponseLevel::ALL {
193 let tc = lookup(pt, ControllerType::P, response);
194 assert_eq!(tc.beta, 0.0, "{pt:?}/{response:?} beta");
195 assert_eq!(tc.c2, 0.0, "{pt:?}/{response:?} c2");
196 assert_eq!(tc.c3, 0.0, "{pt:?}/{response:?} c3");
197 assert!(tc.c1 > 0.0, "{pt:?}/{response:?} c1 should be nonzero");
198 }
199 }
200 }
201
202 #[test]
206 fn non_temperature_process_types_have_zero_pid_column() {
207 let non_temperature = [
208 ProcessType::Flow,
209 ProcessType::PressureLine,
210 ProcessType::PressureVessel,
211 ProcessType::Level,
212 ];
213 for pt in non_temperature {
214 for response in ResponseLevel::ALL {
215 let tc = lookup(pt, ControllerType::Pid, response);
216 assert_eq!(tc.c1, 0.0, "{pt:?}/{response:?} c1");
217 assert_eq!(tc.c2, 0.0, "{pt:?} c2");
218 assert_eq!(tc.c3, 0.0, "{pt:?} c3");
219 assert_eq!(tc.beta, 0.0, "{pt:?} beta");
220 }
221 }
222 }
223
224 #[test]
225 fn temperature_mixing_pid_is_nonzero() {
226 let tc = lookup(
227 ProcessType::TemperatureMixing,
228 ControllerType::Pid,
229 ResponseLevel::Moderate,
230 );
231 assert_eq!(tc.c1, 0.249);
232 assert_eq!(tc.c2, 0.162);
233 assert_eq!(tc.c3, 0.14);
234 assert_eq!(tc.beta, 0.102);
235 }
236
237 #[test]
238 fn temperature_heat_exchange_pid_is_nonzero() {
239 let tc = lookup(
240 ProcessType::TemperatureHeatExchange,
241 ControllerType::Pid,
242 ResponseLevel::Moderate,
243 );
244 assert_eq!(tc.c1, 0.222);
245 assert_eq!(tc.c2, 0.275);
246 assert_eq!(tc.c3, 0.09);
247 assert_eq!(tc.beta, 0.013);
248 }
249
250 #[test]
254 fn pressure_vessel_and_level_share_identical_constants() {
255 for controller in ControllerType::ALL {
256 for response in ResponseLevel::ALL {
257 let vessel = lookup(ProcessType::PressureVessel, controller, response);
258 let level = lookup(ProcessType::Level, controller, response);
259 assert_eq!(vessel, level, "{controller:?}/{response:?}");
260 }
261 }
262 }
263
264 #[test]
265 fn default_cycle_and_noise_tables_have_six_entries() {
266 assert_eq!(DEFAULT_CYCLES_SKIP.len(), 6);
267 assert_eq!(DEFAULT_CYCLES_TEST.len(), 6);
268 assert_eq!(DEFAULT_NOISE_PROTECTION_SECS.len(), 6);
269 }
270
271 #[test]
272 fn response_level_serde_round_trip() {
273 for rl in ResponseLevel::ALL {
274 let json = serde_json::to_string(&rl).unwrap();
275 let back: ResponseLevel = serde_json::from_str(&json).unwrap();
276 assert_eq!(rl, back);
277 }
278 }
279
280 #[test]
281 fn tuning_constants_serde_round_trip() {
282 let tc = lookup(
283 ProcessType::Flow,
284 ControllerType::Pi,
285 ResponseLevel::Aggressive,
286 );
287 let json = serde_json::to_string(&tc).unwrap();
288 let back: TuningConstants = serde_json::from_str(&json).unwrap();
289 assert_eq!(tc, back);
290 }
291}