qualia_core_db/domains/physical/
thermodynamics.rs1#[derive(Clone)]
6pub struct EnsembleState {
7 pub temperature: f64,
8 pub particles: usize,
9 pub total_energy: f64,
10}
11
12pub struct ThermodynamicSampler {
14 pub current_state: EnsembleState,
15}
16
17impl ThermodynamicSampler {
18 pub fn new(initial_temp: f64, particles: usize) -> Self {
19 Self {
20 current_state: EnsembleState {
21 temperature: initial_temp,
22 particles,
23 total_energy: 0.0,
24 },
25 }
26 }
27
28 pub fn metropolis_step(&mut self, proposed_energy: f64, random_uniform: f64) -> bool {
30 let delta_e = proposed_energy - self.current_state.total_energy;
31
32 let k_b = 8.617333262145e-5; let beta = 1.0 / (k_b * self.current_state.temperature);
35
36 let acceptance_probability = if delta_e < 0.0 {
37 1.0
38 } else {
39 (-beta * delta_e).exp()
40 };
41
42 if random_uniform < acceptance_probability {
43 self.current_state.total_energy = proposed_energy;
44 true } else {
46 false }
48 }
49
50 pub fn calculate_gibbs_free_energy(&self, enthalpy: f64, entropy: f64) -> f64 {
52 enthalpy - (self.current_state.temperature * entropy)
54 }
55}
56
57#[derive(Debug, Clone, Copy)]
70pub struct LithiumPack {
71 pub cells_series: u32,
72 pub cells_parallel: u32,
73 pub cell_internal_resistance_ohm: f64,
74 pub cell_capacity_ah: f64,
75}
76
77impl LithiumPack {
78 pub fn cell_ocv(&self, soc: f64) -> f64 {
81 3.0 + (4.2 - 3.0) * soc.clamp(0.0, 1.0)
82 }
83
84 pub fn pack_ocv(&self, soc: f64) -> f64 {
86 self.cells_series as f64 * self.cell_ocv(soc)
87 }
88
89 pub fn pack_resistance(&self) -> f64 {
91 self.cell_internal_resistance_ohm * self.cells_series as f64
92 / self.cells_parallel.max(1) as f64
93 }
94
95 pub fn terminal_voltage(&self, soc: f64, load_current_a: f64) -> f64 {
97 self.pack_ocv(soc) - load_current_a * self.pack_resistance()
98 }
99
100 pub fn deliverable_power(&self, soc: f64, load_current_a: f64) -> f64 {
102 self.terminal_voltage(soc, load_current_a).max(0.0) * load_current_a
103 }
104
105 pub fn pack_capacity_ah(&self) -> f64 {
107 self.cell_capacity_ah * self.cells_parallel as f64
108 }
109}
110
111#[derive(Debug, Clone, Copy)]
114pub struct SolarPanel {
115 pub short_circuit_current_a: f64,
116 pub open_circuit_voltage_v: f64,
117 pub fill_factor: f64,
118}
119
120impl SolarPanel {
121 pub fn current_at(&self, v: f64) -> f64 {
124 if v <= 0.0 {
125 return self.short_circuit_current_a;
126 }
127 if v >= self.open_circuit_voltage_v {
128 return 0.0;
129 }
130 let p = (1.0 / (1.0 - self.fill_factor.clamp(0.05, 0.95))).max(1.0);
131 self.short_circuit_current_a * (1.0 - (v / self.open_circuit_voltage_v).powf(p))
132 }
133
134 pub fn max_power_point(&self, scan_steps: u32) -> (f64, f64, f64) {
138 let mut best = (0.0f64, 0.0f64, 0.0f64);
139 let n = scan_steps.max(2);
140 for k in 1..n {
141 let v = self.open_circuit_voltage_v * k as f64 / n as f64;
142 let i = self.current_at(v);
143 let p = v * i;
144 if p > best.2 {
145 best = (v, i, p);
146 }
147 }
148 best
149 }
150}
151
152pub fn array_mppt_power(panels: &[SolarPanel], scan_steps: u32) -> f64 {
155 panels.iter().map(|p| p.max_power_point(scan_steps).2).sum()
156}
157
158pub fn heat_loss_rate(u_value_w_m2k: f64, area_m2: f64, delta_t_k: f64) -> f64 {
160 u_value_w_m2k * area_m2 * delta_t_k
161}
162
163pub fn phase_change_energy(mass_kg: f64, latent_heat_j_kg: f64) -> f64 {
167 mass_kg * latent_heat_j_kg
168}
169
170pub fn thermal_efficiency(
175 useful_power_w: f64,
176 u_value_w_m2k: f64,
177 area_m2: f64,
178 delta_t_k: f64,
179) -> f64 {
180 let loss = heat_loss_rate(u_value_w_m2k, area_m2, delta_t_k).abs();
181 let useful = useful_power_w.max(0.0);
182 if useful + loss <= 0.0 {
183 return 0.0;
184 }
185 useful / (useful + loss)
186}
187
188#[cfg(test)]
189mod offgrid_energy_tests {
190 use super::*;
191
192 #[test]
193 fn lithium_pack_sags_under_load() {
194 let pack = LithiumPack {
196 cells_series: 4,
197 cells_parallel: 2,
198 cell_internal_resistance_ohm: 0.005,
199 cell_capacity_ah: 100.0,
200 };
201 assert!((pack.pack_ocv(1.0) - 16.8).abs() < 1e-9); assert!((pack.pack_capacity_ah() - 200.0).abs() < 1e-9); let rest = pack.terminal_voltage(0.5, 0.0);
204 let loaded = pack.terminal_voltage(0.5, 50.0);
205 assert!(loaded < rest, "terminal voltage must sag under load");
206 assert!((rest - loaded - 0.5).abs() < 1e-9);
208 assert!(pack.deliverable_power(0.5, 50.0) > 0.0);
209 }
210
211 #[test]
212 fn solar_mpp_is_between_zero_and_isc_voc() {
213 let panel = SolarPanel {
214 short_circuit_current_a: 8.0,
215 open_circuit_voltage_v: 40.0,
216 fill_factor: 0.75,
217 };
218 let (v_mp, i_mp, p_mp) = panel.max_power_point(256);
219 assert!(p_mp > 0.0);
220 assert!(v_mp > 0.0 && v_mp < panel.open_circuit_voltage_v);
221 assert!(i_mp > 0.0 && i_mp < panel.short_circuit_current_a);
222 assert!(p_mp < panel.short_circuit_current_a * panel.open_circuit_voltage_v);
224 let arr = array_mppt_power(&[panel, panel], 256);
226 assert!((arr - 2.0 * p_mp).abs() < 1e-6);
227 }
228
229 #[test]
230 fn thermal_efficiency_drops_with_worse_insulation() {
231 let good = thermal_efficiency(1000.0, 0.5, 10.0, 20.0); let bad = thermal_efficiency(1000.0, 3.0, 10.0, 20.0); assert!(
235 good > bad,
236 "lower U (better insulation) ⇒ higher efficiency"
237 );
238 assert!(good > 0.0 && good < 1.0 && bad > 0.0 && bad < 1.0);
239 assert!((phase_change_energy(2.0, 334_000.0) - 668_000.0).abs() < 1.0); }
242}