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}