use crate::client::proto::common::metrics::Bounds as PbBounds;
use crate::quantity::{Current, Power, Quantity, ReactivePower};
#[derive(Debug, Clone, PartialEq)]
pub struct Bounds<Q: Quantity> {
lower: Option<Q>,
upper: Option<Q>,
}
impl<Q: Quantity> Bounds<Q> {
pub fn new(lower: Option<Q>, upper: Option<Q>) -> Self {
Self { lower, upper }
}
pub fn lower(&self) -> Option<Q> {
self.lower
}
pub fn upper(&self) -> Option<Q> {
self.upper
}
pub fn combine_parallel(&self, other: &Self) -> Vec<Self> {
if self.intersect(other).is_none() {
return vec![self.clone(), other.clone()];
}
let lower = self.lower.and_then(|a| {
other.lower.map(|b| {
if a <= Q::zero() && b <= Q::zero() {
a + b
} else {
a.min(b)
}
})
});
let upper = self.upper.and_then(|a| {
other.upper.map(|b| {
if a >= Q::zero() && b >= Q::zero() {
a + b
} else {
a.max(b)
}
})
});
vec![Bounds { lower, upper }]
}
pub fn intersect(&self, other: &Self) -> Option<Self> {
let lower = Self::map_or_any(Q::max, self.lower, other.lower);
let upper = Self::map_or_any(Q::min, self.upper, other.upper);
if let (Some(lower), Some(upper)) = (lower, upper)
&& lower > upper
{
return None;
}
Some(Bounds { lower, upper })
}
pub fn merge_if_overlapping(&self, other: &Self) -> Option<Self> {
self.intersect(other)?;
Some(Bounds {
lower: self.lower.and_then(|a| other.lower.map(|b| a.min(b))),
upper: self.upper.and_then(|a| other.upper.map(|b| a.max(b))),
})
}
fn map_or_any(f: impl FnOnce(Q, Q) -> Q, a: Option<Q>, b: Option<Q>) -> Option<Q> {
match (a, b) {
(Some(a), Some(b)) => Some(f(a, b)),
(Some(a), None) | (None, Some(a)) => Some(a),
(None, None) => None,
}
}
}
impl<Q: Quantity> From<(Option<Q>, Option<Q>)> for Bounds<Q> {
fn from(bounds: (Option<Q>, Option<Q>)) -> Self {
Self::new(bounds.0, bounds.1)
}
}
impl From<Bounds<Power>> for PbBounds {
fn from(bounds: Bounds<Power>) -> Self {
PbBounds {
lower: bounds.lower.map(|q| q.as_watts()),
upper: bounds.upper.map(|q| q.as_watts()),
}
}
}
impl From<Bounds<Current>> for PbBounds {
fn from(bounds: Bounds<Current>) -> Self {
PbBounds {
lower: bounds.lower.map(|q| q.as_amperes()),
upper: bounds.upper.map(|q| q.as_amperes()),
}
}
}
impl From<Bounds<ReactivePower>> for PbBounds {
fn from(bounds: Bounds<ReactivePower>) -> Self {
PbBounds {
lower: bounds.lower.map(|q| q.as_volt_amperes_reactive()),
upper: bounds.upper.map(|q| q.as_volt_amperes_reactive()),
}
}
}
impl From<PbBounds> for Bounds<Power> {
fn from(pb_bounds: PbBounds) -> Self {
Self::new(
pb_bounds.lower.map(Power::from_watts),
pb_bounds.upper.map(Power::from_watts),
)
}
}
impl From<PbBounds> for Bounds<Current> {
fn from(pb_bounds: PbBounds) -> Self {
Self::new(
pb_bounds.lower.map(Current::from_amperes),
pb_bounds.upper.map(Current::from_amperes),
)
}
}
impl From<PbBounds> for Bounds<ReactivePower> {
fn from(pb_bounds: PbBounds) -> Self {
Self::new(
pb_bounds
.lower
.map(ReactivePower::from_volt_amperes_reactive),
pb_bounds
.upper
.map(ReactivePower::from_volt_amperes_reactive),
)
}
}
pub(crate) fn combine_parallel_sets<Q: Quantity>(
a: &[Bounds<Q>],
b: &[Bounds<Q>],
) -> Vec<Bounds<Q>> {
match (a, b) {
(a, []) | ([], a) => a.to_vec(),
(a, b) => {
let mut result = Vec::new();
for b1 in a {
for b2 in b {
result.extend(b1.combine_parallel(b2));
}
}
squash_bounds_sets(result)
}
}
}
pub(crate) fn intersect_bounds_sets<Q: Quantity>(
a: &[Bounds<Q>],
b: &[Bounds<Q>],
) -> Vec<Bounds<Q>> {
let mut result = Vec::new();
for b1 in a {
for b2 in b {
if let Some(int) = b1.intersect(b2) {
result.push(int);
}
}
}
squash_bounds_sets(result)
}
fn squash_bounds_sets<Q: Quantity>(mut input: Vec<Bounds<Q>>) -> Vec<Bounds<Q>> {
if input.is_empty() {
return input;
}
input.sort_by(|a, b| {
a.lower
.unwrap_or(Q::MIN)
.partial_cmp(&b.lower.unwrap_or(Q::MIN))
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut squashed = Vec::new();
let mut current = input[0].clone();
for next in &input[1..] {
if let Some(merged_bounds) = current.merge_if_overlapping(next) {
current = merged_bounds;
} else {
squashed.push(current);
current = next.clone();
}
}
squashed.push(current);
squashed
}
#[cfg(test)]
mod tests {
use super::{Bounds, combine_parallel_sets, intersect_bounds_sets};
#[test]
fn test_bounds_addition() {
let b1 = Bounds::new(Some(-5.0), Some(5.0));
let b2 = Bounds::new(Some(-3.0), Some(3.0));
assert_eq!(
b1.combine_parallel(&b2),
vec![Bounds::new(Some(-8.0), Some(8.0))]
);
let b1 = Bounds::new(Some(-15.0), Some(-5.0));
let b2 = Bounds::new(Some(-10.0), Some(-2.0));
assert_eq!(
b1.combine_parallel(&b2),
vec![Bounds::new(Some(-25.0), Some(-2.0))]
);
let b1 = Bounds::new(Some(5.0), Some(15.0));
let b2 = Bounds::new(Some(2.0), Some(10.0));
assert_eq!(
b1.combine_parallel(&b2),
vec![Bounds::new(Some(2.0), Some(25.0))]
);
let b1 = Bounds::new(Some(5.0), Some(15.0));
let b2 = Bounds::new(None, Some(10.0));
assert_eq!(
b1.combine_parallel(&b2),
vec![Bounds::new(None, Some(25.0))]
);
let b1 = Bounds::new(Some(5.0), Some(15.0));
let b2 = Bounds::new(Some(-5.0), None);
assert_eq!(
b1.combine_parallel(&b2),
vec![Bounds::new(Some(-5.0), None)]
);
let b1 = Bounds::new(Some(5.0), Some(15.0));
let b2 = Bounds::new(None, None);
assert_eq!(b1.combine_parallel(&b2), vec![Bounds::new(None, None)]);
let b1 = Bounds::new(Some(-10.0), Some(-5.0));
let b2 = Bounds::new(Some(5.0), Some(15.0));
assert_eq!(b1.combine_parallel(&b2), vec![b1, b2]);
}
#[test]
fn test_combine_parallel_sets() {
let b1 = vec![Bounds::new(Some(-5.0), Some(5.0))];
let b2 = vec![
Bounds::new(Some(-5.0), Some(-2.0)),
Bounds::new(Some(2.0), Some(5.0)),
];
let result = combine_parallel_sets(&b1, &b2);
assert_eq!(result, vec![Bounds::new(Some(-10.0), Some(10.0))]);
let b1 = vec![Bounds::new(Some(-5.0), Some(-1.0))];
let b2 = vec![
Bounds::new(Some(-5.0), Some(-2.0)),
Bounds::new(Some(2.0), Some(5.0)),
];
let result = combine_parallel_sets(&b1, &b2);
assert_eq!(
result,
vec![
Bounds::new(Some(-10.0), Some(-1.0)),
Bounds::new(Some(2.0), Some(5.0))
]
);
}
#[test]
fn test_intersect_bounds_sets() {
let vb1 = vec![
Bounds::new(Some(-30.0), Some(-10.0)),
Bounds::new(Some(10.0), Some(30.0)),
];
let vb2 = vec![
Bounds::new(Some(-20.0), Some(0.0)),
Bounds::new(Some(20.0), Some(40.0)),
];
let intersection = intersect_bounds_sets(&vb1, &vb2);
assert_eq!(
intersection,
vec![
Bounds::new(Some(-20.0), Some(-10.0)),
Bounds::new(Some(20.0), Some(30.0)),
]
);
let vb2 = vec![
Bounds::new(Some(-20.0), None),
Bounds::new(None, Some(40.0)),
];
let intersection = intersect_bounds_sets(&vb1, &vb2);
assert_eq!(
intersection,
vec![
Bounds::new(Some(-30.0), Some(-10.0)),
Bounds::new(Some(10.0), Some(30.0)),
]
);
let vb2 = vec![
Bounds::new(None, Some(-20.0)),
Bounds::new(Some(20.0), None),
];
let intersection = intersect_bounds_sets(&vb1, &vb2);
assert_eq!(
intersection,
vec![
Bounds::new(Some(-30.0), Some(-20.0)),
Bounds::new(Some(20.0), Some(30.0)),
]
);
let vb2 = vec![Bounds::new(Some(-25.0), Some(25.0))];
let intersection = intersect_bounds_sets(&vb1, &vb2);
assert_eq!(
intersection,
vec![
Bounds::new(Some(-25.0), Some(-10.0)),
Bounds::new(Some(10.0), Some(25.0)),
]
);
let vb2 = vec![Bounds::new(Some(-5.0), Some(5.0))];
let intersection = intersect_bounds_sets(&vb1, &vb2);
assert_eq!(intersection, vec![]);
}
#[test]
fn intersect_single_point_is_non_empty() {
let a = Bounds::new(Some(5.0), Some(10.0));
let b = Bounds::new(Some(10.0), Some(15.0));
assert_eq!(a.intersect(&b), Some(Bounds::new(Some(10.0), Some(10.0))));
}
#[test]
fn squash_merges_touching_endpoints() {
let a = [Bounds::new(Some(1.0), Some(5.0))];
let b = [Bounds::new(Some(5.0), Some(10.0))];
let result = intersect_bounds_sets(
&[Bounds::new(Some(0.0), Some(20.0))],
&a.iter().chain(b.iter()).cloned().collect::<Vec<_>>(),
);
assert_eq!(result, vec![Bounds::new(Some(1.0), Some(10.0))]);
}
#[test]
fn combine_parallel_preserves_fully_unbounded() {
let a = Bounds::<f32>::new(None, None);
let b = Bounds::<f32>::new(None, None);
assert_eq!(a.combine_parallel(&b), vec![Bounds::new(None, None)]);
}
}