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