Skip to main content

bacnet_objects/averaging/
window.rs

1//! The sliding sample window behind the Averaging statistics (Clause 12.5).
2//!
3//! The window keeps the most recent Window_Samples attempts in arrival order.
4//! Each slot holds either the REAL that was sampled or a marker for an attempt
5//! that produced no value. Minimum_Value, Maximum_Value and
6//! Average_Value cover the valid slots only; Attempted_Samples counts every
7//! slot and Valid_Samples the valid ones, so subtracting the second from the
8//! first tells a reader how many attempts in the window failed (Clauses
9//! 12.5.11 and 12.5.12).
10
11use std::collections::VecDeque;
12use std::sync::Arc;
13use std::time::Duration;
14
15use bacnet_types::error::Error;
16
17use super::schedule::{SampleSchedule, MIN_SAMPLE_PERIOD};
18use crate::common;
19use crate::traits::MonotonicClock;
20
21/// Largest Window_Samples an Averaging object accepts.
22///
23/// Clause 12.5.15 sets a floor of 15 samples per window but no ceiling, while
24/// the window keeps one slot per sample, so the buffer needs a bound. 1,440
25/// slots hold one sample a minute for a whole day, the longest window building
26/// automation commonly averages over, at about 11 KiB per object (eight bytes
27/// a slot). A write or setter above it fails with PROPERTY /
28/// VALUE_OUT_OF_RANGE.
29pub const MAX_WINDOW_SAMPLES: u32 = 1_440;
30
31/// Window_Samples of a new Averaging object: the 15-sample floor every
32/// implementation has to support (Clause 12.5.15).
33pub const DEFAULT_WINDOW_SAMPLES: u32 = 15;
34
35/// Window_Interval of a new Averaging object, in seconds. Fifteen minutes
36/// spreads the default 15 samples one minute apart.
37pub const DEFAULT_WINDOW_INTERVAL: u32 = 900;
38
39// The default meets the 15-sample floor, and the bound admits it.
40const _: () = assert!(DEFAULT_WINDOW_SAMPLES >= 15 && MAX_WINDOW_SAMPLES >= DEFAULT_WINDOW_SAMPLES);
41
42/// Statistics over the valid slots, recomputed on every change.
43#[derive(Debug, Clone, Copy)]
44struct Statistics {
45    minimum: f32,
46    maximum: f32,
47    average: f32,
48    valid: u32,
49}
50
51impl Statistics {
52    /// The values with no valid sample in the window: Clause 12.5 gives
53    /// positive infinity for Minimum_Value, NaN for Average_Value and negative
54    /// infinity for Maximum_Value. `f32::NAN` is one fixed bit pattern, so a
55    /// COV comparison of two empty windows sees no change.
56    const EMPTY: Self = Self {
57        minimum: f32::INFINITY,
58        maximum: f32::NEG_INFINITY,
59        average: f32::NAN,
60        valid: 0,
61    };
62}
63
64/// A bounded buffer of the most recent sample attempts, and the schedule the
65/// server fills it on.
66#[derive(Debug, Clone)]
67pub(super) struct SampleWindow {
68    /// Window_Interval, in seconds.
69    interval: u32,
70    /// Window_Samples: the most slots the buffer holds.
71    capacity: u32,
72    /// Oldest first; `None` marks an attempt that yielded no value.
73    slots: VecDeque<Option<f32>>,
74    statistics: Statistics,
75    /// Restarted by every [`reset`](Self::reset), which every route that
76    /// empties the window goes through.
77    schedule: SampleSchedule,
78}
79
80impl SampleWindow {
81    pub(super) fn new() -> Self {
82        Self {
83            interval: DEFAULT_WINDOW_INTERVAL,
84            capacity: DEFAULT_WINDOW_SAMPLES,
85            slots: VecDeque::new(),
86            statistics: Statistics::EMPTY,
87            schedule: SampleSchedule::default(),
88        }
89    }
90
91    pub(super) fn interval(&self) -> u32 {
92        self.interval
93    }
94
95    pub(super) fn capacity(&self) -> u32 {
96        self.capacity
97    }
98
99    pub(super) fn minimum(&self) -> f32 {
100        self.statistics.minimum
101    }
102
103    pub(super) fn maximum(&self) -> f32 {
104        self.statistics.maximum
105    }
106
107    pub(super) fn average(&self) -> f32 {
108        self.statistics.average
109    }
110
111    /// Attempts in the current window, which stops growing at Window_Samples.
112    pub(super) fn attempted(&self) -> u32 {
113        // The buffer never holds more than `capacity` (a u32) slots.
114        self.slots.len() as u32
115    }
116
117    pub(super) fn valid(&self) -> u32 {
118        self.statistics.valid
119    }
120
121    /// Set Window_Interval and discard the samples. Zero fails with PROPERTY /
122    /// VALUE_OUT_OF_RANGE: the clause leaves the minimum to the implementation,
123    /// and a window with no length has no sample spacing.
124    pub(super) fn set_interval(&mut self, interval: u32) -> Result<(), Error> {
125        if interval == 0 {
126            return Err(common::value_out_of_range_error());
127        }
128        self.interval = interval;
129        self.reset();
130        Ok(())
131    }
132
133    /// Set Window_Samples and discard the samples. Zero, or more than
134    /// [`MAX_WINDOW_SAMPLES`], fails with PROPERTY / VALUE_OUT_OF_RANGE.
135    pub(super) fn set_capacity(&mut self, capacity: u32) -> Result<(), Error> {
136        if !(1..=MAX_WINDOW_SAMPLES).contains(&capacity) {
137            return Err(common::value_out_of_range_error());
138        }
139        self.capacity = capacity;
140        self.reset();
141        self.slots.shrink_to(capacity as usize);
142        Ok(())
143    }
144
145    /// Record one attempt, dropping the oldest once the window is full.
146    pub(super) fn push(&mut self, sample: Option<f32>) {
147        if self.slots.len() >= self.capacity as usize {
148            self.slots.pop_front();
149        }
150        self.slots.push_back(sample);
151        self.recompute();
152    }
153
154    /// Discard every sample: the counts go to zero and the statistics to
155    /// their empty-window values. Scheduled sampling starts over from now.
156    pub(super) fn reset(&mut self) {
157        self.slots.clear();
158        self.statistics = Statistics::EMPTY;
159        let period = self.period();
160        self.schedule.restart(period);
161    }
162
163    /// Spacing of scheduled samples: Window_Interval / Window_Samples, but no
164    /// shorter than [`MIN_SAMPLE_PERIOD`].
165    pub(super) fn period(&self) -> Duration {
166        // Window_Samples is never zero: construction and set_capacity keep it
167        // at least one.
168        (Duration::from_secs(self.interval.into()) / self.capacity).max(MIN_SAMPLE_PERIOD)
169    }
170
171    /// Bind or remove the monotonic clock and restart the schedule. The
172    /// samples already taken stay.
173    pub(super) fn bind_clock(&mut self, clock: Option<Arc<MonotonicClock>>) {
174        let period = self.period();
175        self.schedule.bind(clock, period);
176    }
177
178    /// When the next scheduled sample is due, once a clock is bound.
179    pub(super) fn next_due(&self) -> Option<Duration> {
180        self.schedule.next_due()
181    }
182
183    /// Whether a scheduled sample is due at `now`, stepping the schedule past
184    /// it when it is. Recording the sample is the caller's job.
185    pub(super) fn take_due(&mut self, now: Duration) -> bool {
186        let period = self.period();
187        self.schedule.take_due(now, period)
188    }
189
190    fn recompute(&mut self) {
191        let mut statistics = Statistics::EMPTY;
192        let mut sum = 0.0f64;
193        for &value in self.slots.iter().flatten() {
194            statistics.minimum = statistics.minimum.min(value);
195            statistics.maximum = statistics.maximum.max(value);
196            sum += f64::from(value);
197            statistics.valid += 1;
198        }
199        if statistics.valid > 0 {
200            // Summing the whole window each time in f64 avoids the drift an
201            // incremental update builds up as samples leave the window. The
202            // clamp keeps the rounded mean inside the sampled range.
203            let average = (sum / f64::from(statistics.valid)) as f32;
204            statistics.average = average.clamp(statistics.minimum, statistics.maximum);
205        }
206        self.statistics = statistics;
207    }
208}