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