Skip to main content

frequenz_microgrid/
bounds.rs

1// License: MIT
2// Copyright © 2026 Frequenz Energy-as-a-Service GmbH
3
4//! A representation of Bounds for any metric.
5
6use crate::client::proto::common::metrics::Bounds as PbBounds;
7use crate::quantity::{Current, Power, Quantity, ReactivePower};
8
9/// A set of lower and upper bounds for any metric.
10#[derive(Debug, Clone, PartialEq)]
11pub struct Bounds<Q: Quantity> {
12    /// The lower bound.
13    /// If None, there is no lower bound.
14    lower: Option<Q>,
15    /// The upper bound.
16    /// If None, there is no upper bound.
17    upper: Option<Q>,
18}
19
20impl<Q: Quantity> Bounds<Q> {
21    /// Creates a new `Bounds` with the given lower and upper bounds.
22    pub fn new(lower: Option<Q>, upper: Option<Q>) -> Self {
23        Self { lower, upper }
24    }
25
26    /// Returns the lower bound.
27    pub fn lower(&self) -> Option<Q> {
28        self.lower
29    }
30
31    /// Returns the upper bound.
32    pub fn upper(&self) -> Option<Q> {
33        self.upper
34    }
35
36    /// Combines two bounds as if their components were connected in parallel.
37    pub fn combine_parallel(&self, other: &Self) -> Vec<Self> {
38        if self.intersect(other).is_none() {
39            return vec![self.clone(), other.clone()];
40        }
41        // Lower side: if both lowers are ≤ 0, the components can both
42        // discharge, so the combined floor is the sum (more negative).
43        // Otherwise at least one range sits entirely above zero and the
44        // combined floor is just the lower of the two individual floors.
45        let lower = self.lower.and_then(|a| {
46            other.lower.map(|b| {
47                if a <= Q::zero() && b <= Q::zero() {
48                    a + b
49                } else {
50                    a.min(b)
51                }
52            })
53        });
54        // Upper side: mirror of the above — both ≥ 0 means both can charge and
55        // contributions add; otherwise take the higher of the two.
56        let upper = self.upper.and_then(|a| {
57            other.upper.map(|b| {
58                if a >= Q::zero() && b >= Q::zero() {
59                    a + b
60                } else {
61                    a.max(b)
62                }
63            })
64        });
65        vec![Bounds { lower, upper }]
66    }
67
68    /// Returns the intersection of `self` and `other`, or `None` if the
69    /// intersection is empty.
70    pub fn intersect(&self, other: &Self) -> Option<Self> {
71        let lower = Self::map_or_any(Q::max, self.lower, other.lower);
72        let upper = Self::map_or_any(Q::min, self.upper, other.upper);
73        if let (Some(lower), Some(upper)) = (lower, upper)
74            && lower > upper
75        {
76            return None;
77        }
78        Some(Bounds { lower, upper })
79    }
80
81    /// If `self` and `other` overlap, returns the smallest single interval
82    /// that contains both; otherwise returns `None`.
83    pub fn merge_if_overlapping(&self, other: &Self) -> Option<Self> {
84        self.intersect(other)?;
85        Some(Bounds {
86            lower: self.lower.and_then(|a| other.lower.map(|b| a.min(b))),
87            upper: self.upper.and_then(|a| other.upper.map(|b| a.max(b))),
88        })
89    }
90
91    /// Combines two `Option<Q>` values with `f`, treating `None` as the
92    /// identity: if exactly one side is `Some`, that value is returned
93    /// unchanged. Only `(None, None)` yields `None`.
94    fn map_or_any(f: impl FnOnce(Q, Q) -> Q, a: Option<Q>, b: Option<Q>) -> Option<Q> {
95        match (a, b) {
96            (Some(a), Some(b)) => Some(f(a, b)),
97            (Some(a), None) | (None, Some(a)) => Some(a),
98            (None, None) => None,
99        }
100    }
101}
102
103impl<Q: Quantity> From<(Option<Q>, Option<Q>)> for Bounds<Q> {
104    fn from(bounds: (Option<Q>, Option<Q>)) -> Self {
105        Self::new(bounds.0, bounds.1)
106    }
107}
108
109impl From<Bounds<Power>> for PbBounds {
110    fn from(bounds: Bounds<Power>) -> Self {
111        PbBounds {
112            lower: bounds.lower.map(|q| q.as_watts()),
113            upper: bounds.upper.map(|q| q.as_watts()),
114        }
115    }
116}
117
118impl From<Bounds<Current>> for PbBounds {
119    fn from(bounds: Bounds<Current>) -> Self {
120        PbBounds {
121            lower: bounds.lower.map(|q| q.as_amperes()),
122            upper: bounds.upper.map(|q| q.as_amperes()),
123        }
124    }
125}
126
127impl From<Bounds<ReactivePower>> for PbBounds {
128    fn from(bounds: Bounds<ReactivePower>) -> Self {
129        PbBounds {
130            lower: bounds.lower.map(|q| q.as_volt_amperes_reactive()),
131            upper: bounds.upper.map(|q| q.as_volt_amperes_reactive()),
132        }
133    }
134}
135
136impl From<PbBounds> for Bounds<Power> {
137    fn from(pb_bounds: PbBounds) -> Self {
138        Self::new(
139            pb_bounds.lower.map(Power::from_watts),
140            pb_bounds.upper.map(Power::from_watts),
141        )
142    }
143}
144
145impl From<PbBounds> for Bounds<Current> {
146    fn from(pb_bounds: PbBounds) -> Self {
147        Self::new(
148            pb_bounds.lower.map(Current::from_amperes),
149            pb_bounds.upper.map(Current::from_amperes),
150        )
151    }
152}
153
154impl From<PbBounds> for Bounds<ReactivePower> {
155    fn from(pb_bounds: PbBounds) -> Self {
156        Self::new(
157            pb_bounds
158                .lower
159                .map(ReactivePower::from_volt_amperes_reactive),
160            pb_bounds
161                .upper
162                .map(ReactivePower::from_volt_amperes_reactive),
163        )
164    }
165}
166
167/// Combines two sets of bounds from components connected in parallel.
168pub(crate) fn combine_parallel_sets<Q: Quantity>(
169    a: &[Bounds<Q>],
170    b: &[Bounds<Q>],
171) -> Vec<Bounds<Q>> {
172    match (a, b) {
173        (a, []) | ([], a) => a.to_vec(),
174        (a, b) => {
175            let mut result = Vec::new();
176            for b1 in a {
177                for b2 in b {
178                    result.extend(b1.combine_parallel(b2));
179                }
180            }
181            squash_bounds_sets(result)
182        }
183    }
184}
185
186/// Intersects two sets of bounds together, returning the intersection of the
187/// given sets.
188///
189/// This is used for calculating the combined bounds of two components connected
190/// in series.
191pub(crate) fn intersect_bounds_sets<Q: Quantity>(
192    a: &[Bounds<Q>],
193    b: &[Bounds<Q>],
194) -> Vec<Bounds<Q>> {
195    let mut result = Vec::new();
196    for b1 in a {
197        for b2 in b {
198            if let Some(int) = b1.intersect(b2) {
199                result.push(int);
200            }
201        }
202    }
203    squash_bounds_sets(result)
204}
205
206/// Merges overlapping bounds into disjoint intervals.
207fn squash_bounds_sets<Q: Quantity>(mut input: Vec<Bounds<Q>>) -> Vec<Bounds<Q>> {
208    if input.is_empty() {
209        return input;
210    }
211
212    input.sort_by(|a, b| {
213        a.lower
214            .unwrap_or(Q::MIN)
215            .partial_cmp(&b.lower.unwrap_or(Q::MIN))
216            .unwrap_or(std::cmp::Ordering::Equal)
217    });
218
219    let mut squashed = Vec::new();
220    let mut current = input[0].clone();
221
222    for next in &input[1..] {
223        if let Some(merged_bounds) = current.merge_if_overlapping(next) {
224            current = merged_bounds;
225        } else {
226            squashed.push(current);
227            current = next.clone();
228        }
229    }
230    squashed.push(current);
231
232    squashed
233}
234
235#[cfg(test)]
236mod tests {
237    use super::{Bounds, combine_parallel_sets, intersect_bounds_sets};
238
239    #[test]
240    fn test_bounds_addition() {
241        let b1 = Bounds::new(Some(-5.0), Some(5.0));
242        let b2 = Bounds::new(Some(-3.0), Some(3.0));
243        assert_eq!(
244            b1.combine_parallel(&b2),
245            vec![Bounds::new(Some(-8.0), Some(8.0))]
246        );
247
248        let b1 = Bounds::new(Some(-15.0), Some(-5.0));
249        let b2 = Bounds::new(Some(-10.0), Some(-2.0));
250        assert_eq!(
251            b1.combine_parallel(&b2),
252            vec![Bounds::new(Some(-25.0), Some(-2.0))]
253        );
254
255        let b1 = Bounds::new(Some(5.0), Some(15.0));
256        let b2 = Bounds::new(Some(2.0), Some(10.0));
257        assert_eq!(
258            b1.combine_parallel(&b2),
259            vec![Bounds::new(Some(2.0), Some(25.0))]
260        );
261
262        let b1 = Bounds::new(Some(5.0), Some(15.0));
263        let b2 = Bounds::new(None, Some(10.0));
264        assert_eq!(
265            b1.combine_parallel(&b2),
266            vec![Bounds::new(None, Some(25.0))]
267        );
268
269        let b1 = Bounds::new(Some(5.0), Some(15.0));
270        let b2 = Bounds::new(Some(-5.0), None);
271        assert_eq!(
272            b1.combine_parallel(&b2),
273            vec![Bounds::new(Some(-5.0), None)]
274        );
275
276        let b1 = Bounds::new(Some(5.0), Some(15.0));
277        let b2 = Bounds::new(None, None);
278        assert_eq!(b1.combine_parallel(&b2), vec![Bounds::new(None, None)]);
279
280        let b1 = Bounds::new(Some(-10.0), Some(-5.0));
281        let b2 = Bounds::new(Some(5.0), Some(15.0));
282        assert_eq!(b1.combine_parallel(&b2), vec![b1, b2]);
283    }
284
285    #[test]
286    fn test_combine_parallel_sets() {
287        let b1 = vec![Bounds::new(Some(-5.0), Some(5.0))];
288        let b2 = vec![
289            Bounds::new(Some(-5.0), Some(-2.0)),
290            Bounds::new(Some(2.0), Some(5.0)),
291        ];
292        let result = combine_parallel_sets(&b1, &b2);
293        assert_eq!(result, vec![Bounds::new(Some(-10.0), Some(10.0))]);
294
295        let b1 = vec![Bounds::new(Some(-5.0), Some(-1.0))];
296        let b2 = vec![
297            Bounds::new(Some(-5.0), Some(-2.0)),
298            Bounds::new(Some(2.0), Some(5.0)),
299        ];
300        let result = combine_parallel_sets(&b1, &b2);
301        assert_eq!(
302            result,
303            vec![
304                Bounds::new(Some(-10.0), Some(-1.0)),
305                Bounds::new(Some(2.0), Some(5.0))
306            ]
307        );
308    }
309
310    #[test]
311    fn test_intersect_bounds_sets() {
312        let vb1 = vec![
313            Bounds::new(Some(-30.0), Some(-10.0)),
314            Bounds::new(Some(10.0), Some(30.0)),
315        ];
316        let vb2 = vec![
317            Bounds::new(Some(-20.0), Some(0.0)),
318            Bounds::new(Some(20.0), Some(40.0)),
319        ];
320        let intersection = intersect_bounds_sets(&vb1, &vb2);
321        assert_eq!(
322            intersection,
323            vec![
324                Bounds::new(Some(-20.0), Some(-10.0)),
325                Bounds::new(Some(20.0), Some(30.0)),
326            ]
327        );
328
329        let vb2 = vec![
330            Bounds::new(Some(-20.0), None),
331            Bounds::new(None, Some(40.0)),
332        ];
333        let intersection = intersect_bounds_sets(&vb1, &vb2);
334        assert_eq!(
335            intersection,
336            vec![
337                Bounds::new(Some(-30.0), Some(-10.0)),
338                Bounds::new(Some(10.0), Some(30.0)),
339            ]
340        );
341
342        let vb2 = vec![
343            Bounds::new(None, Some(-20.0)),
344            Bounds::new(Some(20.0), None),
345        ];
346        let intersection = intersect_bounds_sets(&vb1, &vb2);
347        assert_eq!(
348            intersection,
349            vec![
350                Bounds::new(Some(-30.0), Some(-20.0)),
351                Bounds::new(Some(20.0), Some(30.0)),
352            ]
353        );
354
355        let vb2 = vec![Bounds::new(Some(-25.0), Some(25.0))];
356        let intersection = intersect_bounds_sets(&vb1, &vb2);
357        assert_eq!(
358            intersection,
359            vec![
360                Bounds::new(Some(-25.0), Some(-10.0)),
361                Bounds::new(Some(10.0), Some(25.0)),
362            ]
363        );
364
365        let vb2 = vec![Bounds::new(Some(-5.0), Some(5.0))];
366        let intersection = intersect_bounds_sets(&vb1, &vb2);
367        assert_eq!(intersection, vec![]);
368    }
369
370    /// Bounds are closed intervals: intersecting at a shared endpoint yields a
371    /// degenerate single-point interval rather than an empty result.
372    #[test]
373    fn intersect_single_point_is_non_empty() {
374        let a = Bounds::new(Some(5.0), Some(10.0));
375        let b = Bounds::new(Some(10.0), Some(15.0));
376        assert_eq!(a.intersect(&b), Some(Bounds::new(Some(10.0), Some(10.0))));
377    }
378
379    /// Closed-interval semantics in `squash`: two intervals that touch at a
380    /// single endpoint merge into one.
381    #[test]
382    fn squash_merges_touching_endpoints() {
383        let a = [Bounds::new(Some(1.0), Some(5.0))];
384        let b = [Bounds::new(Some(5.0), Some(10.0))];
385        // `intersect_bounds_sets` runs the pairwise intersect through squash.
386        let result = intersect_bounds_sets(
387            &[Bounds::new(Some(0.0), Some(20.0))],
388            &a.iter().chain(b.iter()).cloned().collect::<Vec<_>>(),
389        );
390        assert_eq!(result, vec![Bounds::new(Some(1.0), Some(10.0))]);
391    }
392
393    /// Fully-unbounded inputs are preserved through `combine_parallel`:
394    /// `(−∞, ∞) ⊕ (−∞, ∞)` is still `(−∞, ∞)`, not empty.
395    #[test]
396    fn combine_parallel_preserves_fully_unbounded() {
397        let a = Bounds::<f32>::new(None, None);
398        let b = Bounds::<f32>::new(None, None);
399        assert_eq!(a.combine_parallel(&b), vec![Bounds::new(None, None)]);
400    }
401}