elevator_core/energy.rs
1//! Simplified energy modeling for elevators.
2//!
3//! Provides an [`EnergyProfile`](crate::energy::EnergyProfile) that
4//! parameterizes per-tick energy costs and an
5//! [`EnergyMetrics`](crate::energy::EnergyMetrics) accumulator for tracking
6//! consumption and regeneration over time. The pure function
7//! `compute_tick_energy` calculates consumed and regenerated energy for a
8//! single tick.
9
10use serde::{Deserialize, Serialize};
11
12/// Per-elevator energy cost parameters.
13///
14/// Attach to an elevator entity by setting the
15/// [`energy_profile`](crate::config::ElevatorConfig::energy_profile) field on
16/// [`ElevatorConfig`](crate::config::ElevatorConfig) before constructing the
17/// simulation. The energy system automatically initializes [`EnergyMetrics`]
18/// if not already present.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct EnergyProfile {
21 /// Energy consumed per tick while idle (doors closed, stationary).
22 pub idle_cost_per_tick: f64,
23 /// Base energy consumed per tick while moving.
24 pub move_cost_per_tick: f64,
25 /// Multiplier applied to current load weight when moving.
26 pub weight_factor: f64,
27 /// Fraction of consumed energy regenerated when descending (0.0 - 1.0).
28 pub regen_factor: f64,
29}
30
31impl EnergyProfile {
32 /// Create a new energy profile with the given parameters.
33 #[must_use]
34 pub const fn new(
35 idle_cost_per_tick: f64,
36 move_cost_per_tick: f64,
37 weight_factor: f64,
38 regen_factor: f64,
39 ) -> Self {
40 Self {
41 idle_cost_per_tick,
42 move_cost_per_tick,
43 weight_factor,
44 regen_factor,
45 }
46 }
47
48 /// Energy consumed per tick while idle.
49 #[must_use]
50 pub const fn idle_cost_per_tick(&self) -> f64 {
51 self.idle_cost_per_tick
52 }
53
54 /// Base energy consumed per tick while moving.
55 #[must_use]
56 pub const fn move_cost_per_tick(&self) -> f64 {
57 self.move_cost_per_tick
58 }
59
60 /// Multiplier applied to current load weight when moving.
61 #[must_use]
62 pub const fn weight_factor(&self) -> f64 {
63 self.weight_factor
64 }
65
66 /// Fraction of consumed energy regenerated when descending.
67 #[must_use]
68 pub const fn regen_factor(&self) -> f64 {
69 self.regen_factor
70 }
71}
72
73/// Accumulated energy metrics for a single elevator.
74#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct EnergyMetrics {
76 /// Total energy consumed over the simulation.
77 pub(crate) total_consumed: f64,
78 /// Total energy regenerated over the simulation.
79 pub(crate) total_regenerated: f64,
80 /// Number of ticks with energy activity recorded.
81 pub(crate) ticks_tracked: u32,
82}
83
84impl EnergyMetrics {
85 /// Total energy consumed.
86 #[must_use]
87 pub const fn total_consumed(&self) -> f64 {
88 self.total_consumed
89 }
90
91 /// Total energy regenerated.
92 #[must_use]
93 pub const fn total_regenerated(&self) -> f64 {
94 self.total_regenerated
95 }
96
97 /// Number of ticks with energy activity recorded.
98 #[must_use]
99 pub const fn ticks_tracked(&self) -> u32 {
100 self.ticks_tracked
101 }
102
103 /// Net energy: consumed minus regenerated.
104 #[must_use]
105 pub const fn net_energy(&self) -> f64 {
106 self.total_consumed - self.total_regenerated
107 }
108
109 /// Record a tick's energy consumption and regeneration.
110 pub(crate) fn record(&mut self, consumed: f64, regenerated: f64) {
111 self.total_consumed += consumed;
112 self.total_regenerated += regenerated;
113 self.ticks_tracked += 1;
114 }
115}
116
117/// Compute consumed and regenerated energy for a single tick.
118///
119/// Returns `(consumed, regenerated)`.
120///
121/// - Idle: consumed = `idle_cost_per_tick`, regenerated = 0.
122/// - Moving: consumed = `move_cost_per_tick + weight_factor * current_load`.
123/// - Moving downward (velocity < 0): regenerated = `consumed * regen_factor`.
124#[must_use]
125pub(crate) fn compute_tick_energy(
126 profile: &EnergyProfile,
127 is_moving: bool,
128 current_load: f64,
129 velocity: f64,
130) -> (f64, f64) {
131 if !is_moving {
132 return (profile.idle_cost_per_tick, 0.0);
133 }
134
135 let consumed = crate::fp::fma(
136 profile.weight_factor,
137 current_load,
138 profile.move_cost_per_tick,
139 );
140 let regenerated = if velocity < 0.0 {
141 consumed * profile.regen_factor
142 } else {
143 0.0
144 };
145
146 (consumed, regenerated)
147}