hics/hics.rs
1//&# HICS
2//&
3//&This example implements a Heat Index Control System (hics).
4//&More precisely, the example implements a system to keep the heat index (a quantity depending on the real temperature and the humidity via a certain mapping) in a room located in a region with a hot and damp climate bearable.\
5//&The system specification is that the heat index has to be brought down periodically to a certain value within a window of tolerance depending on the daytime by first trying to dehumidify the room and if that does not suffice to also cool down the room.
6//&Furthermore, a thermohygrometer and a clock shall be accessible for an implementation.\
7//&The hics-implementation presented in this example measures temeprature and humidity as well as the time to decide if it has to take action.
8//&And if it actuates something, it waits for it to take effect in order to decide whether to repeat or to go idle for a period.
9//&Moreover it follows the structuring-suggestions from [Why Functional Programming Matters](https://www.cse.chalmers.se/~rjmh/Papers/whyfp.pdf).
10//&There they describe how the use of higher-order functions and lazy evaluation can greatly improve the modularity of programs.
11//&They illustrate that point by modularizing on-demand computations of perhaps infinite objects like real numbers and game trees, exploiting that in languages with first-class functions and lazy evaluation everything is a generator in some sense.
12//&As the hics described above shares that 'on-demand computation' aspect - most notably, it measures (that is, reads out the thermohygrometer) on demand - these modularization techniques will apply in that case, too.
13//&Adapting the techniques to rspl and Rust yields a modular implementation of the addressed hics in Rust.
14//&
15//&The intention of the example is to demonstrate rspl's applicability in demand-driven[^1] programming (with generators, of course).
16//&
17//&Now that we have said what we are going to implement and why, let us explain our techniques before presenting the code applying those techniques.
18//&To this end, we split the following discussion into two parts.
19//&First, we introduce the general design pattern of encoding generators in rspl.
20//&This is done without referring to the specific example of control systems.
21//&Then, second, before discussing the actual hics code in place, we briefly discuss the code's overall structure.\
22//&So, first, rspl's stream processors can encode some sort of generator: regarding a sufficiently general definition of generator one can consider any stream processor a generator because output is generated on demand in an incremental manner.
23//&But even for the more specific definition of generators as functions which can remember state infromation between calls, rspl's stream processors offer an encoding.
24//&To understand how, first note that rspl's stream processors would implement the `Fn`-trait if Rust allowed users to arbitrarily implement that trait.
25//&This is because implementing a stream processor from `A` to `B` in rspl corresponds to defining a function from `Stream<A>` to `Stream<B>`.
26//&So, if `A` is the input signature and `B` the yield type of a generator, then that generator could be encoded as stream processor from `A` to `B` provided that a way to remember state information is available.
27//&Now, the perhaps most common approach to state in functional programming - and rspl is functional programming - is state-passing style[^2].
28//&And, in fact, state passing is applicable to rspl's stream processors.
29//&One way is to construct a stream processor by a Rust function with a single parameter representing the state, returning a stream processor that captures a computation of the perhaps manipulated state within a (lazy) recursive call like in
30//&```rust
31//&fn generator<'a, S, A, B>(mut state: S) -> StreamProcessor<'a, A, B> {
32//& ...;
33//& StreamProcessor::get(|a: A| StreamProcessor::put(..., || generator(...state...)))
34//&}
35//&```
36//&The returned stream processor of such a function is a generator where the dots are the body of that generator.
37//&(Note that if `A` is `()`, it can make sense to omit the `get`-part.)\
38//&After having discussed the encoding of generators as stream processors, let us have a look at the structure of our hics implementation.
39//&Essentially, it consists of four parts.
40//&The first one is a module encapsulating general aspects of control systems.
41//&The second and the third part specialize to and use those aspects for heat index controlling.
42//&Particularly, the second part implements the control system interface while the third part is a driver responsible for executing that implementation according to the measure-on-demand strategy.
43//&Finally, the fourth part is the main-function simulating the hics environment and setting up the driver for the hics.
44//&
45//&Let us now walk through the code together.
46
47//&```rust
48mod control {
49 use rspl::streams::infinite_lists::InfiniteList;
50 use rspl::streams::Stream;
51 use rspl::StreamProcessor;
52
53 use std::thread;
54 use std::time::Duration;
55
56 // This is a definition of control systems. Importantly, it requires a `meter` to generate
57 // measurements on demand and this is statically enforced by the typing.
58 pub trait System<'a, Space> {
59 fn meter(&self) -> StreamProcessor<'a, (), Space>;
60 fn reference(&self) -> f64;
61 fn quantity(&self, position: Space) -> f64;
62 fn controller(self, deviation: f64, status: f64, position: Space) -> Self;
63 }
64
65 pub trait Strategy<'a, Space> {
66 fn execute(self, cs: impl System<'a, Space>, epsilon: f64);
67 }
68
69 pub struct MeasureOnDemand {
70 pub dwell_time: Duration,
71 }
72
73 impl<'a, Space: 'a + Copy> Strategy<'a, Space> for MeasureOnDemand {
74 fn execute(self, mut cs: impl System<'a, Space>, epsilon: f64) {
75 let mut status;
76
77 // Here the measurements are generated (lazily).
78 let mut positions = cs.meter().eval(InfiniteList::constant(()));
79
80 loop {
81 // Here the actual measurement is made.
82 positions = positions.tail();
83 let position = *positions.head();
84
85 status = cs.quantity(position);
86 let setpoint = cs.reference();
87 let deviation = status - setpoint;
88
89 if f64::abs(deviation) < epsilon {
90 break;
91 }
92
93 cs = cs.controller(deviation, status, position);
94
95 thread::sleep(self.dwell_time);
96 }
97 }
98 }
99}
100
101use control::Strategy;
102
103use rspl::streams::infinite_lists::InfiniteList;
104use rspl::streams::Stream;
105use rspl::StreamProcessor;
106
107use std::sync::atomic::{AtomicU64, Ordering};
108use std::sync::{Arc, Mutex};
109use std::thread;
110use std::time::Duration;
111
112use crossbeam::channel;
113use crossbeam::channel::Sender;
114
115// This constant is the window of tolerance for the heat index.
116const EPSILON: f64 = 0.5;
117
118const REFERENCE_HEAT_INDEX_DAY: f64 = 91.0;
119const REFERENCE_HEAT_INDEX_NIGHT: f64 = 83.0;
120
121const MINIMAL_TEMPERATURE: f64 = 80.0;
122const MINIMAL_HUMIDITY: f64 = 50.0;
123
124const INITIAL_TEMPERATURE: f64 = 87.0;
125const INITIAL_HUMIDITY: f64 = 72.0;
126
127const ACTUATOR_DECREASE: HeatIndexSpace = HeatIndexSpace {
128 temperature: 0.25,
129 humidity: 1.5,
130};
131const NATURAL_INCREASE: HeatIndexSpace = HeatIndexSpace {
132 temperature: 0.02,
133 humidity: 0.1,
134};
135
136// This block defines time-related constants. In particular, note that the `TICK` is intended to
137// represent 10 real seconds.
138const TICK_LENGTH: u64 = 5; // in (real) millis
139const TICK: u64 = 1;
140const DAY: u64 = 8640 * TICK;
141const DWELL_TIME: u64 = 6 * TICK;
142const CONTROL_PERIOD: u64 = 180 * TICK;
143const NATURAL_INCREASE_PERIOD: u64 = 3 * TICK;
144
145const UNSAFE_BARRIER: usize = 100_000;
146const SERVICE_BARRIER: usize = UNSAFE_BARRIER - 5000;
147
148type HeatIndex = f64;
149type Time = u64;
150type Clock = AtomicU64;
151
152#[derive(Copy, Clone)]
153struct HeatIndexSpace {
154 temperature: f64, // in degree Fahrenheit
155 humidity: f64, // in percent
156}
157
158// This type defines the output signals of the hics. A signal is either status information
159// (`Show(...)`) or orders for the actuator to execute (`Dehumidfy` and `Cool`).
160enum HeatIndexSignal {
161 Show(Time, HeatIndex),
162 Dehumidify,
163 Cool,
164}
165
166// This type is the actual hics. Essentially, it is the communication interface to its environment.
167#[derive(Clone)]
168struct Hics {
169 clock_finger: Arc<Clock>,
170 thermohygrometer_finger: Arc<Mutex<HeatIndexSpace>>,
171 signals_s: Sender<HeatIndexSignal>,
172}
173
174impl<'a> control::System<'a, HeatIndexSpace> for Hics {
175 fn meter(&self) -> StreamProcessor<'a, (), HeatIndexSpace> {
176 fn read_out<'a, X: 'a + Copy>(finger: Arc<Mutex<X>>) -> StreamProcessor<'a, (), X> {
177 StreamProcessor::Put(
178 *Arc::clone(&finger).lock().unwrap(),
179 Box::new(|| read_out(finger)),
180 )
181 }
182
183 read_out(Arc::clone(&self.thermohygrometer_finger))
184 }
185 fn reference(&self) -> f64 {
186 let time = self.clock_finger.load(Ordering::SeqCst);
187
188 if time % DAY < DAY / 2 {
189 REFERENCE_HEAT_INDEX_DAY
190 } else {
191 REFERENCE_HEAT_INDEX_NIGHT
192 }
193 }
194 fn quantity(&self, position: HeatIndexSpace) -> f64 {
195 // The body is the heat index formula from https://en.wikipedia.org/wiki/Heat_index.
196 const C_1: f64 = -42.379;
197 const C_2: f64 = 2.049_015_23;
198 const C_3: f64 = 10.143_331_27;
199 const C_4: f64 = -0.224_755_41;
200 const C_5: f64 = -0.006_837_83;
201 const C_6: f64 = -0.054_817_17;
202 const C_7: f64 = 0.001_228_74;
203 const C_8: f64 = 0.000_852_82;
204 const C_9: f64 = -0.000_001_99;
205
206 let t = position.temperature;
207 let r = position.humidity;
208
209 C_1 + C_2 * t
210 + C_3 * r
211 + C_4 * t * r
212 + C_5 * t * t
213 + C_6 * r * r
214 + C_7 * t * t * r
215 + C_8 * t * r * r
216 + C_9 * t * t * r * r
217 }
218 fn controller(self, deviation: f64, status: f64, position: HeatIndexSpace) -> Self {
219 let time = self.clock_finger.load(Ordering::SeqCst);
220 self.signals_s
221 .send(HeatIndexSignal::Show(time, status))
222 .unwrap();
223
224 if deviation > 0.0 {
225 if position.humidity > MINIMAL_HUMIDITY {
226 self.signals_s.send(HeatIndexSignal::Dehumidify).unwrap();
227 } else if position.temperature > MINIMAL_TEMPERATURE {
228 self.signals_s.send(HeatIndexSignal::Cool).unwrap();
229 }
230 }
231
232 self
233 }
234}
235
236#[allow(clippy::assertions_on_constants)]
237fn driver(hics: Hics) {
238 fn control<'a>(hics: Hics, mut counter: usize) -> StreamProcessor<'a, (), usize> {
239 control::MeasureOnDemand {
240 dwell_time: Duration::from_millis(DWELL_TIME * TICK_LENGTH),
241 }
242 .execute(hics.clone(), EPSILON);
243
244 counter += 1;
245
246 StreamProcessor::Put(counter, Box::new(move || control(hics, counter)))
247 }
248
249 assert!(UNSAFE_BARRIER > SERVICE_BARRIER);
250
251 // Here the runs of the hics are generated (lazily).
252 let mut runs = control(hics, 0).eval(InfiniteList::constant(()));
253
254 loop {
255 thread::sleep(Duration::from_millis(CONTROL_PERIOD * TICK_LENGTH));
256
257 // Here, an iteration of the hics is started.
258 runs = runs.tail();
259 let run_count = *runs.head();
260
261 if run_count > SERVICE_BARRIER {
262 if run_count > UNSAFE_BARRIER {
263 break;
264 }
265 println!(
266 "Warning: Service needed. ({} runs > {} runs)",
267 run_count, SERVICE_BARRIER
268 );
269 }
270 }
271}
272
273fn main() {
274 fn print_heat_index_event(time: Time, heat_index: HeatIndex) {
275 let red = |x| (x * 16.0 - 1350.0) as u8;
276 let green = 25;
277 let blue = |x| (1450.0 - x * 16.0) as u8;
278 // The cryptic part of the following `format!(...)` is just an ANSI escape code to get
279 // `format!({:.1}°F, heat_index)` with a truecolor R(ed)G(reen)B(lue) background.
280 let degree = format!(
281 "\x1b[48;2;{};{};{}m{:.1}°F\x1b[0m",
282 red(heat_index),
283 green,
284 blue(heat_index),
285 heat_index,
286 );
287
288 let to_minutes = |x| (x as f64 / 6.0) % 1440.0;
289 let time = format!("6am plus {:.1} minutes", to_minutes(time));
290
291 println!("Heat Index Event: {} at {}", degree, time);
292 }
293
294 let clock = Arc::new(AtomicU64::new(0));
295 let clock_finger = Arc::clone(&clock);
296
297 let thermohygrometer = Arc::new(Mutex::new(HeatIndexSpace {
298 temperature: INITIAL_TEMPERATURE,
299 humidity: INITIAL_HUMIDITY,
300 }));
301 let thermohygrometer_finger = Arc::clone(&thermohygrometer);
302
303 let (signals_s, signals_r) = channel::unbounded();
304
305 let hics = Hics {
306 clock_finger,
307 thermohygrometer_finger,
308 signals_s,
309 };
310
311 let _clock_simulator = thread::spawn(move || loop {
312 thread::sleep(Duration::from_millis(TICK_LENGTH));
313
314 clock.store(clock.load(Ordering::SeqCst) + TICK, Ordering::SeqCst);
315 });
316
317 let _thermohygrometer_simulator = thread::spawn(move || {
318 let thermohygrometer_finger = Arc::clone(&thermohygrometer);
319
320 // This is the climate effect actuated by the hics.
321 let _actuator_simulator = thread::spawn(move || loop {
322 let signal = signals_r.recv().unwrap();
323
324 let mut position = thermohygrometer_finger.lock().unwrap();
325 match signal {
326 HeatIndexSignal::Show(time, heat_index) => print_heat_index_event(time, heat_index),
327 HeatIndexSignal::Dehumidify => position.humidity -= ACTUATOR_DECREASE.humidity,
328 HeatIndexSignal::Cool => position.temperature -= ACTUATOR_DECREASE.temperature,
329 }
330 });
331
332 // This is the climate effect actuated by nature.
333 loop {
334 thread::sleep(Duration::from_millis(NATURAL_INCREASE_PERIOD * TICK_LENGTH));
335
336 let mut position = thermohygrometer.lock().unwrap();
337 position.humidity += NATURAL_INCREASE.humidity;
338 position.temperature += NATURAL_INCREASE.temperature;
339 }
340 });
341
342 driver(hics);
343}
344//&```
345
346//&Finally, let us conclude with the key take-away:
347//&rspl can encode generators and is hence suited for demand-driven programming.
348//&However, it is not so clear why to use rspl to encode generators in general.
349//&Indeed, this is also not quite what we wanted to show.
350//&The idea is rather to show that rspl's stream processors can naturally incorporate demand-driven programming making them particularly useful to stream-processing problems with demand-driven aspects.
351//&It might be that the hics implemented here is not the best possible example to do so.
352//&But it is the best we could come up with as of yet which is real-world enough while still being focused on the `put`-construct of rspl.
353
354//&[^1]: Look at [Codata in Action](https://www.microsoft.com/en-us/research/uploads/prod/2020/01/CoDataInAction.pdf) for some more explanation on that term.
355//&[^2]: Also see the concept of monads which kind of subsumes foobar-passing style.