1use crate::client::proto::common::metrics::Bounds as PbBounds;
7use crate::quantity::{Current, Power, Quantity, ReactivePower};
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct Bounds<Q: Quantity> {
12 lower: Option<Q>,
15 upper: Option<Q>,
18}
19
20impl<Q: Quantity> std::fmt::Display for Bounds<Q> {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 f.write_fmt(format_args!(
23 "[{}, {}]",
24 self.lower
25 .map_or_else(|| String::from("None"), |x| x.to_string()),
26 self.upper
27 .map_or_else(|| String::from("None"), |x| x.to_string()),
28 ))
29 }
30}
31
32impl<Q: Quantity> Bounds<Q> {
33 pub fn new(lower: Option<Q>, upper: Option<Q>) -> Self {
35 Self { lower, upper }
36 }
37
38 pub fn lower(&self) -> Option<Q> {
40 self.lower
41 }
42
43 pub fn upper(&self) -> Option<Q> {
45 self.upper
46 }
47
48 pub fn combine_parallel(&self, other: &Self) -> Vec<Self> {
50 if self.intersect(other).is_none() {
51 return vec![self.clone(), other.clone()];
52 }
53 let lower = self.lower.and_then(|a| {
58 other.lower.map(|b| {
59 if a <= Q::zero() && b <= Q::zero() {
60 a + b
61 } else {
62 a.min(b)
63 }
64 })
65 });
66 let upper = self.upper.and_then(|a| {
69 other.upper.map(|b| {
70 if a >= Q::zero() && b >= Q::zero() {
71 a + b
72 } else {
73 a.max(b)
74 }
75 })
76 });
77 vec![Bounds { lower, upper }]
78 }
79
80 pub fn intersect(&self, other: &Self) -> Option<Self> {
83 let lower = Self::map_or_any(Q::max, self.lower, other.lower);
84 let upper = Self::map_or_any(Q::min, self.upper, other.upper);
85 if let (Some(lower), Some(upper)) = (lower, upper)
86 && lower > upper
87 {
88 return None;
89 }
90 Some(Bounds { lower, upper })
91 }
92
93 pub fn merge_if_overlapping(&self, other: &Self) -> Option<Self> {
96 self.intersect(other)?;
97 Some(Bounds {
98 lower: self.lower.and_then(|a| other.lower.map(|b| a.min(b))),
99 upper: self.upper.and_then(|a| other.upper.map(|b| a.max(b))),
100 })
101 }
102
103 fn map_or_any(f: impl FnOnce(Q, Q) -> Q, a: Option<Q>, b: Option<Q>) -> Option<Q> {
107 match (a, b) {
108 (Some(a), Some(b)) => Some(f(a, b)),
109 (Some(a), None) | (None, Some(a)) => Some(a),
110 (None, None) => None,
111 }
112 }
113}
114
115impl<Q: Quantity> From<(Option<Q>, Option<Q>)> for Bounds<Q> {
116 fn from(bounds: (Option<Q>, Option<Q>)) -> Self {
117 Self::new(bounds.0, bounds.1)
118 }
119}
120
121impl From<Bounds<Power>> for PbBounds {
122 fn from(bounds: Bounds<Power>) -> Self {
123 PbBounds {
124 lower: bounds.lower.map(|q| q.as_watts()),
125 upper: bounds.upper.map(|q| q.as_watts()),
126 }
127 }
128}
129
130impl From<Bounds<Current>> for PbBounds {
131 fn from(bounds: Bounds<Current>) -> Self {
132 PbBounds {
133 lower: bounds.lower.map(|q| q.as_amperes()),
134 upper: bounds.upper.map(|q| q.as_amperes()),
135 }
136 }
137}
138
139impl From<Bounds<ReactivePower>> for PbBounds {
140 fn from(bounds: Bounds<ReactivePower>) -> Self {
141 PbBounds {
142 lower: bounds.lower.map(|q| q.as_volt_amperes_reactive()),
143 upper: bounds.upper.map(|q| q.as_volt_amperes_reactive()),
144 }
145 }
146}
147
148impl From<PbBounds> for Bounds<Power> {
149 fn from(pb_bounds: PbBounds) -> Self {
150 Self::new(
151 pb_bounds.lower.map(Power::from_watts),
152 pb_bounds.upper.map(Power::from_watts),
153 )
154 }
155}
156
157impl From<PbBounds> for Bounds<Current> {
158 fn from(pb_bounds: PbBounds) -> Self {
159 Self::new(
160 pb_bounds.lower.map(Current::from_amperes),
161 pb_bounds.upper.map(Current::from_amperes),
162 )
163 }
164}
165
166impl From<PbBounds> for Bounds<ReactivePower> {
167 fn from(pb_bounds: PbBounds) -> Self {
168 Self::new(
169 pb_bounds
170 .lower
171 .map(ReactivePower::from_volt_amperes_reactive),
172 pb_bounds
173 .upper
174 .map(ReactivePower::from_volt_amperes_reactive),
175 )
176 }
177}
178
179pub(crate) fn combine_parallel_sets<Q: Quantity>(
181 a: &[Bounds<Q>],
182 b: &[Bounds<Q>],
183) -> Vec<Bounds<Q>> {
184 match (a, b) {
185 (a, []) | ([], a) => a.to_vec(),
186 (a, b) => {
187 let mut result = Vec::new();
188 for b1 in a {
189 for b2 in b {
190 result.extend(b1.combine_parallel(b2));
191 }
192 }
193 squash_bounds_sets(result)
194 }
195 }
196}
197
198pub(crate) fn intersect_bounds_sets<Q: Quantity>(
204 a: &[Bounds<Q>],
205 b: &[Bounds<Q>],
206) -> Vec<Bounds<Q>> {
207 let mut result = Vec::new();
208 for b1 in a {
209 for b2 in b {
210 if let Some(int) = b1.intersect(b2) {
211 result.push(int);
212 }
213 }
214 }
215 squash_bounds_sets(result)
216}
217
218fn squash_bounds_sets<Q: Quantity>(mut input: Vec<Bounds<Q>>) -> Vec<Bounds<Q>> {
220 if input.is_empty() {
221 return input;
222 }
223
224 input.sort_by(|a, b| {
225 a.lower
226 .unwrap_or(Q::MIN)
227 .partial_cmp(&b.lower.unwrap_or(Q::MIN))
228 .unwrap_or(std::cmp::Ordering::Equal)
229 });
230
231 let mut squashed = Vec::new();
232 let mut current = input[0].clone();
233
234 for next in &input[1..] {
235 if let Some(merged_bounds) = current.merge_if_overlapping(next) {
236 current = merged_bounds;
237 } else {
238 squashed.push(current);
239 current = next.clone();
240 }
241 }
242 squashed.push(current);
243
244 squashed
245}
246
247#[cfg(test)]
248mod tests {
249 use super::{Bounds, combine_parallel_sets, intersect_bounds_sets};
250 use crate::quantity::Power;
251
252 #[test]
253 fn test_bounds_addition() {
254 let b1 = Bounds::new(Some(-5.0), Some(5.0));
255 let b2 = Bounds::new(Some(-3.0), Some(3.0));
256 assert_eq!(
257 b1.combine_parallel(&b2),
258 vec![Bounds::new(Some(-8.0), Some(8.0))]
259 );
260
261 let b1 = Bounds::new(Some(-15.0), Some(-5.0));
262 let b2 = Bounds::new(Some(-10.0), Some(-2.0));
263 assert_eq!(
264 b1.combine_parallel(&b2),
265 vec![Bounds::new(Some(-25.0), Some(-2.0))]
266 );
267
268 let b1 = Bounds::new(Some(5.0), Some(15.0));
269 let b2 = Bounds::new(Some(2.0), Some(10.0));
270 assert_eq!(
271 b1.combine_parallel(&b2),
272 vec![Bounds::new(Some(2.0), Some(25.0))]
273 );
274
275 let b1 = Bounds::new(Some(5.0), Some(15.0));
276 let b2 = Bounds::new(None, Some(10.0));
277 assert_eq!(
278 b1.combine_parallel(&b2),
279 vec![Bounds::new(None, Some(25.0))]
280 );
281
282 let b1 = Bounds::new(Some(5.0), Some(15.0));
283 let b2 = Bounds::new(Some(-5.0), None);
284 assert_eq!(
285 b1.combine_parallel(&b2),
286 vec![Bounds::new(Some(-5.0), None)]
287 );
288
289 let b1 = Bounds::new(Some(5.0), Some(15.0));
290 let b2 = Bounds::new(None, None);
291 assert_eq!(b1.combine_parallel(&b2), vec![Bounds::new(None, None)]);
292
293 let b1 = Bounds::new(Some(-10.0), Some(-5.0));
294 let b2 = Bounds::new(Some(5.0), Some(15.0));
295 assert_eq!(b1.combine_parallel(&b2), vec![b1, b2]);
296 }
297
298 #[test]
299 fn test_combine_parallel_sets() {
300 let b1 = vec![Bounds::new(Some(-5.0), Some(5.0))];
301 let b2 = vec![
302 Bounds::new(Some(-5.0), Some(-2.0)),
303 Bounds::new(Some(2.0), Some(5.0)),
304 ];
305 let result = combine_parallel_sets(&b1, &b2);
306 assert_eq!(result, vec![Bounds::new(Some(-10.0), Some(10.0))]);
307
308 let b1 = vec![Bounds::new(Some(-5.0), Some(-1.0))];
309 let b2 = vec![
310 Bounds::new(Some(-5.0), Some(-2.0)),
311 Bounds::new(Some(2.0), Some(5.0)),
312 ];
313 let result = combine_parallel_sets(&b1, &b2);
314 assert_eq!(
315 result,
316 vec![
317 Bounds::new(Some(-10.0), Some(-1.0)),
318 Bounds::new(Some(2.0), Some(5.0))
319 ]
320 );
321 }
322
323 #[test]
324 fn test_intersect_bounds_sets() {
325 let vb1 = vec![
326 Bounds::new(Some(-30.0), Some(-10.0)),
327 Bounds::new(Some(10.0), Some(30.0)),
328 ];
329 let vb2 = vec![
330 Bounds::new(Some(-20.0), Some(0.0)),
331 Bounds::new(Some(20.0), Some(40.0)),
332 ];
333 let intersection = intersect_bounds_sets(&vb1, &vb2);
334 assert_eq!(
335 intersection,
336 vec![
337 Bounds::new(Some(-20.0), Some(-10.0)),
338 Bounds::new(Some(20.0), Some(30.0)),
339 ]
340 );
341
342 let vb2 = vec![
343 Bounds::new(Some(-20.0), None),
344 Bounds::new(None, Some(40.0)),
345 ];
346 let intersection = intersect_bounds_sets(&vb1, &vb2);
347 assert_eq!(
348 intersection,
349 vec![
350 Bounds::new(Some(-30.0), Some(-10.0)),
351 Bounds::new(Some(10.0), Some(30.0)),
352 ]
353 );
354
355 let vb2 = vec![
356 Bounds::new(None, Some(-20.0)),
357 Bounds::new(Some(20.0), None),
358 ];
359 let intersection = intersect_bounds_sets(&vb1, &vb2);
360 assert_eq!(
361 intersection,
362 vec![
363 Bounds::new(Some(-30.0), Some(-20.0)),
364 Bounds::new(Some(20.0), Some(30.0)),
365 ]
366 );
367
368 let vb2 = vec![Bounds::new(Some(-25.0), Some(25.0))];
369 let intersection = intersect_bounds_sets(&vb1, &vb2);
370 assert_eq!(
371 intersection,
372 vec![
373 Bounds::new(Some(-25.0), Some(-10.0)),
374 Bounds::new(Some(10.0), Some(25.0)),
375 ]
376 );
377
378 let vb2 = vec![Bounds::new(Some(-5.0), Some(5.0))];
379 let intersection = intersect_bounds_sets(&vb1, &vb2);
380 assert_eq!(intersection, vec![]);
381 }
382
383 #[test]
386 fn intersect_single_point_is_non_empty() {
387 let a = Bounds::new(Some(5.0), Some(10.0));
388 let b = Bounds::new(Some(10.0), Some(15.0));
389 assert_eq!(a.intersect(&b), Some(Bounds::new(Some(10.0), Some(10.0))));
390 }
391
392 #[test]
395 fn squash_merges_touching_endpoints() {
396 let a = [Bounds::new(Some(1.0), Some(5.0))];
397 let b = [Bounds::new(Some(5.0), Some(10.0))];
398 let result = intersect_bounds_sets(
400 &[Bounds::new(Some(0.0), Some(20.0))],
401 &a.iter().chain(b.iter()).cloned().collect::<Vec<_>>(),
402 );
403 assert_eq!(result, vec![Bounds::new(Some(1.0), Some(10.0))]);
404 }
405
406 #[test]
409 fn combine_parallel_preserves_fully_unbounded() {
410 let a = Bounds::<f32>::new(None, None);
411 let b = Bounds::<f32>::new(None, None);
412 assert_eq!(a.combine_parallel(&b), vec![Bounds::new(None, None)]);
413 }
414
415 #[test]
416 fn display_renders_both_bounds() {
417 let b = Bounds::new(Some(-5.0_f32), Some(5.0_f32));
418 assert_eq!(b.to_string(), "[-5, 5]");
419 }
420
421 #[test]
422 fn display_renders_missing_lower_as_none() {
423 let b = Bounds::new(None, Some(5.0_f32));
424 assert_eq!(b.to_string(), "[None, 5]");
425 }
426
427 #[test]
428 fn display_renders_missing_upper_as_none() {
429 let b = Bounds::new(Some(-5.0_f32), None);
430 assert_eq!(b.to_string(), "[-5, None]");
431 }
432
433 #[test]
434 fn display_renders_fully_unbounded_as_none_none() {
435 let b = Bounds::<f32>::new(None, None);
436 assert_eq!(b.to_string(), "[None, None]");
437 }
438
439 #[test]
440 fn display_uses_inner_quantity_formatting() {
441 let b = Bounds::new(
442 Some(Power::from_kilowatts(-1.0)),
443 Some(Power::from_kilowatts(2.0)),
444 );
445 assert_eq!(b.to_string(), "[-1 kW, 2 kW]");
446 }
447}