Skip to main content

oxiphysics_materials/
energy_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Energy materials module: Li-ion batteries, fuel cells, solar cells,
5//! thermoelectrics, supercapacitors, hydrogen storage, nuclear materials,
6//! and piezoelectric energy harvesting.
7//!
8//! All functions use SI units unless otherwise stated.
9
10#![allow(dead_code)]
11#![allow(unused_imports)]
12#![allow(clippy::too_many_arguments)]
13
14use std::f64::consts::PI;
15
16/// Universal gas constant (J/mol·K).
17const R: f64 = 8.314;
18/// Faraday constant (C/mol).
19const F_FARADAY: f64 = 96_485.0;
20/// Boltzmann constant (J/K).
21const K_B: f64 = 1.380649e-23;
22/// Elementary charge (C).
23const Q_E: f64 = 1.602176634e-19;
24/// Planck constant (J·s).
25const H_PLANCK: f64 = 6.62607015e-34;
26/// Speed of light (m/s).
27const C_LIGHT: f64 = 2.99792458e8;
28
29// ---------------------------------------------------------------------------
30// Lithium-Ion Battery — Butler-Volmer Kinetics & Diffusion
31// ---------------------------------------------------------------------------
32
33/// Parameters for a Li-ion battery electrode (half-cell).
34#[derive(Clone, Debug)]
35pub struct LiIonElectrode {
36    /// Exchange current density i₀ (A/m²).
37    pub i0: f64,
38    /// Transfer coefficient alpha (dimensionless, typically 0.5).
39    pub alpha: f64,
40    /// Temperature (K).
41    pub temperature: f64,
42    /// Electrode thickness (m).
43    pub thickness: f64,
44    /// Solid-phase diffusion coefficient Ds (m²/s).
45    pub d_solid: f64,
46    /// Electrolyte diffusion coefficient De (m²/s).
47    pub d_electrolyte: f64,
48    /// Maximum solid concentration cs_max (mol/m³).
49    pub cs_max: f64,
50    /// Electrode volume fraction of active material.
51    pub epsilon_s: f64,
52    /// Particle radius (m).
53    pub particle_radius: f64,
54    /// Open circuit potential function (stoichiometry -> V).
55    pub ocp_a: f64,
56    /// OCP slope.
57    pub ocp_b: f64,
58}
59
60impl LiIonElectrode {
61    /// Typical LiCoO₂ cathode defaults.
62    pub fn lco_cathode() -> Self {
63        Self {
64            i0: 2.0,
65            alpha: 0.5,
66            temperature: 298.15,
67            thickness: 80.0e-6,
68            d_solid: 1.0e-14,
69            d_electrolyte: 7.5e-10,
70            cs_max: 51_410.0,
71            epsilon_s: 0.5,
72            particle_radius: 2.0e-6,
73            ocp_a: 4.2,
74            ocp_b: -0.5,
75        }
76    }
77
78    /// Graphite anode defaults.
79    pub fn graphite_anode() -> Self {
80        Self {
81            i0: 3.6,
82            alpha: 0.5,
83            temperature: 298.15,
84            thickness: 100.0e-6,
85            d_solid: 3.9e-14,
86            d_electrolyte: 7.5e-10,
87            cs_max: 26_390.0,
88            epsilon_s: 0.471,
89            particle_radius: 5.0e-6,
90            ocp_a: 0.2,
91            ocp_b: 0.1,
92        }
93    }
94
95    /// Butler-Volmer current density (A/m²) as function of overpotential eta (V).
96    pub fn butler_volmer(&self, eta: f64) -> f64 {
97        let f_rt = F_FARADAY / (R * self.temperature);
98        self.i0 * ((self.alpha * f_rt * eta).exp() - ((self.alpha - 1.0) * f_rt * eta).exp())
99    }
100
101    /// Linearised BV (low overpotential approximation): i ≈ i₀ * F/(R*T) * eta.
102    pub fn linear_butler_volmer(&self, eta: f64) -> f64 {
103        self.i0 * F_FARADAY / (R * self.temperature) * eta
104    }
105
106    /// Open circuit potential (OCP) as function of state-of-charge x (0..1).
107    pub fn ocp(&self, x: f64) -> f64 {
108        self.ocp_a + self.ocp_b * x
109    }
110
111    /// Solid diffusion time constant (s): tau = R_p^2 / (15 * Ds).
112    pub fn diffusion_time_constant(&self) -> f64 {
113        self.particle_radius * self.particle_radius / (15.0 * self.d_solid)
114    }
115
116    /// Effective reaction surface area per unit volume (m⁻¹).
117    pub fn specific_interfacial_area(&self) -> f64 {
118        3.0 * self.epsilon_s / self.particle_radius
119    }
120
121    /// Capacity per unit area (A·s/m²) at 1C rate.
122    pub fn areal_capacity(&self) -> f64 {
123        self.cs_max * F_FARADAY * self.epsilon_s * self.thickness
124    }
125
126    /// State of charge from surface concentration cs_surf / cs_max.
127    pub fn state_of_charge(&self, cs_surf: f64) -> f64 {
128        (cs_surf / self.cs_max).clamp(0.0, 1.0)
129    }
130
131    /// Tafel slope (V/decade) for the electrode reaction.
132    pub fn tafel_slope(&self) -> f64 {
133        R * self.temperature / (self.alpha * F_FARADAY) * 10.0f64.ln()
134    }
135}
136
137/// Battery cell model combining anode and cathode.
138#[derive(Clone, Debug)]
139pub struct LiIonCell {
140    /// Cathode electrode.
141    pub cathode: LiIonElectrode,
142    /// Anode electrode.
143    pub anode: LiIonElectrode,
144    /// Electrolyte resistance (Ohm·m²).
145    pub r_electrolyte: f64,
146    /// Contact resistance (Ohm·m²).
147    pub r_contact: f64,
148    /// Nominal capacity (A·h).
149    pub capacity_ah: f64,
150}
151
152impl LiIonCell {
153    /// Standard 18650 LCO/graphite cell.
154    pub fn nmc18650() -> Self {
155        Self {
156            cathode: LiIonElectrode::lco_cathode(),
157            anode: LiIonElectrode::graphite_anode(),
158            r_electrolyte: 5.0e-5,
159            r_contact: 5.0e-6,
160            capacity_ah: 2.5,
161        }
162    }
163
164    /// Cell open circuit voltage.
165    pub fn ocv(&self, soc_cathode: f64, soc_anode: f64) -> f64 {
166        self.cathode.ocp(soc_cathode) - self.anode.ocp(soc_anode)
167    }
168
169    /// Terminal voltage during discharge at current density j (A/m²).
170    pub fn terminal_voltage(&self, j: f64, soc: f64) -> f64 {
171        let ocv = self.cathode.ocp(soc) - self.anode.ocp(1.0 - soc);
172        let eta_c = self.cathode.butler_volmer(j / self.cathode.i0);
173        let eta_a = self.anode.butler_volmer(-j / self.anode.i0);
174        let v_ir = j * (self.r_electrolyte + self.r_contact);
175        ocv - eta_c + eta_a - v_ir
176    }
177
178    /// C-rate given current I (A) and capacity.
179    pub fn c_rate(&self, current_a: f64) -> f64 {
180        current_a / self.capacity_ah
181    }
182
183    /// Ragone plot: specific energy (Wh/kg) vs specific power (W/kg).
184    pub fn ragone_point(&self, c_rate: f64, cell_mass_kg: f64) -> (f64, f64) {
185        let current = c_rate * self.capacity_ah;
186        let voltage = 3.6; // Average discharge voltage
187        let power = current * voltage;
188        let energy = self.capacity_ah * voltage;
189        (energy / cell_mass_kg, power / cell_mass_kg)
190    }
191}
192
193// ---------------------------------------------------------------------------
194// Fuel Cell — Nafion Proton Conductivity and GDL Transport
195// ---------------------------------------------------------------------------
196
197/// Nafion membrane properties for PEM fuel cells.
198#[derive(Clone, Debug)]
199pub struct NafionMembrane {
200    /// Membrane thickness (m).
201    pub thickness: f64,
202    /// Water content lambda (H₂O molecules per SO₃⁻ group).
203    pub water_content: f64,
204    /// Temperature (K).
205    pub temperature: f64,
206    /// Equivalent weight (g/mol SO₃⁻).
207    pub eq_weight: f64,
208    /// Dry density (kg/m³).
209    pub dry_density: f64,
210}
211
212impl NafionMembrane {
213    /// Nafion 117 default parameters.
214    pub fn nafion117() -> Self {
215        Self {
216            thickness: 183.0e-6,
217            water_content: 14.0,
218            temperature: 353.15,
219            eq_weight: 1100.0,
220            dry_density: 2100.0,
221        }
222    }
223
224    /// Springer proton conductivity (S/m) as function of temperature and water content.
225    pub fn proton_conductivity(&self) -> f64 {
226        let lambda = self.water_content;
227        let t = self.temperature;
228        // Springer et al. (1991)
229        (0.005139 * lambda - 0.00326) * (1268.0 * (1.0 / 303.15 - 1.0 / t)).exp() * 1000.0 // S/m conversion from S/cm
230    }
231
232    /// Membrane resistance (Ohm·m²).
233    pub fn area_specific_resistance(&self) -> f64 {
234        self.thickness / self.proton_conductivity()
235    }
236
237    /// Water diffusivity in Nafion (m²/s) — Motupally correlation.
238    pub fn water_diffusivity(&self) -> f64 {
239        let lambda = self.water_content;
240        let t = self.temperature;
241        let d_w = if lambda < 3.0 {
242            1e-10 * (0.87 * (3.0 - lambda) + 2.95 * lambda)
243        } else if lambda < 17.0 {
244            1e-10 * (2.95 + 1.00 * (lambda - 3.0))
245        } else {
246            1e-10 * (1.54 + 0.198 * lambda)
247        };
248        d_w * (2416.0 * (1.0 / 303.15 - 1.0 / t)).exp()
249    }
250
251    /// Sorption isotherm: equilibrium lambda vs. relative humidity a_w.
252    pub fn sorption_isotherm(&self, a_w: f64) -> f64 {
253        // Springer model
254        0.043 + 17.81 * a_w - 39.85 * a_w * a_w + 36.0 * a_w * a_w * a_w
255    }
256
257    /// Electroosmotic drag coefficient (H₂O/H⁺) at given lambda.
258    pub fn electroosmotic_drag(&self) -> f64 {
259        2.5 * self.water_content / 22.0
260    }
261}
262
263/// Gas diffusion layer (GDL) transport model.
264#[derive(Clone, Debug)]
265pub struct GasDiffusionLayer {
266    /// Porosity (0..1).
267    pub porosity: f64,
268    /// Tortuosity.
269    pub tortuosity: f64,
270    /// Thickness (m).
271    pub thickness: f64,
272    /// Contact angle (rad) — for liquid water flooding.
273    pub contact_angle: f64,
274    /// Permeability (m²).
275    pub permeability: f64,
276}
277
278impl GasDiffusionLayer {
279    /// Toray carbon paper TGP-H-120 defaults.
280    pub fn toray_tgp_h120() -> Self {
281        Self {
282            porosity: 0.78,
283            tortuosity: 1.5,
284            thickness: 370.0e-6,
285            contact_angle: 110.0f64.to_radians(),
286            permeability: 7.5e-12,
287        }
288    }
289
290    /// Effective diffusivity via Bruggeman model: D_eff = D_bulk * eps^1.5.
291    pub fn effective_diffusivity(&self, d_bulk: f64) -> f64 {
292        d_bulk * self.porosity.powf(1.5)
293    }
294
295    /// Knudsen diffusivity for small pores (m²/s).
296    ///
297    /// `d_pore`: mean pore diameter (m). `m_molar`: molar mass (kg/mol).
298    pub fn knudsen_diffusivity(&self, d_pore: f64, m_molar: f64, temperature: f64) -> f64 {
299        (d_pore / 3.0) * (8.0 * R * temperature / (PI * m_molar)).sqrt()
300    }
301
302    /// Darcy velocity (m/s) under pressure gradient dp_dx (Pa/m).
303    pub fn darcy_velocity(&self, viscosity: f64, dp_dx: f64) -> f64 {
304        self.permeability / viscosity * dp_dx
305    }
306
307    /// Capillary pressure (Pa) for a pore of diameter d_pore.
308    pub fn capillary_pressure(&self, d_pore: f64, surface_tension: f64) -> f64 {
309        4.0 * surface_tension * self.contact_angle.cos().abs() / d_pore
310    }
311}
312
313/// Polarisation curve model for a PEM fuel cell.
314#[derive(Clone, Debug)]
315pub struct PemFuelCell {
316    /// Nafion membrane.
317    pub membrane: NafionMembrane,
318    /// Cathode GDL.
319    pub cathode_gdl: GasDiffusionLayer,
320    /// Anode GDL.
321    pub anode_gdl: GasDiffusionLayer,
322    /// Cathode exchange current density (A/m²).
323    pub i0_c: f64,
324    /// Anode exchange current density (A/m²).
325    pub i0_a: f64,
326    /// Cathode transfer coefficient.
327    pub alpha_c: f64,
328    /// Nernst open circuit voltage (V) at standard conditions.
329    pub e_nernst: f64,
330    /// Temperature (K).
331    pub temperature: f64,
332}
333
334impl PemFuelCell {
335    /// Standard hydrogen/air PEM fuel cell.
336    pub fn standard() -> Self {
337        Self {
338            membrane: NafionMembrane::nafion117(),
339            cathode_gdl: GasDiffusionLayer::toray_tgp_h120(),
340            anode_gdl: GasDiffusionLayer::toray_tgp_h120(),
341            i0_c: 1.0e-4,
342            i0_a: 10.0,
343            alpha_c: 0.5,
344            e_nernst: 1.229,
345            temperature: 353.15,
346        }
347    }
348
349    /// Activation overpotential at cathode (V) for current density j (A/m²).
350    pub fn cathode_activation_overpotential(&self, j: f64) -> f64 {
351        R * self.temperature / (self.alpha_c * F_FARADAY) * (j / self.i0_c).abs().ln()
352    }
353
354    /// Ohmic overpotential (V) for current density j (A/m²).
355    pub fn ohmic_overpotential(&self, j: f64) -> f64 {
356        j * self.membrane.area_specific_resistance()
357    }
358
359    /// Limiting current density (A/m²) for oxygen transport.
360    pub fn limiting_current_density(&self, c_bulk: f64, d_eff: f64) -> f64 {
361        4.0 * F_FARADAY * d_eff * c_bulk / self.cathode_gdl.thickness
362    }
363
364    /// Cell voltage (V) at current density j.
365    pub fn cell_voltage(&self, j: f64) -> f64 {
366        let eta_act = self.cathode_activation_overpotential(j);
367        let eta_ohm = self.ohmic_overpotential(j);
368        (self.e_nernst - eta_act - eta_ohm).max(0.0)
369    }
370
371    /// Power density (W/m²) at current density j.
372    pub fn power_density(&self, j: f64) -> f64 {
373        j * self.cell_voltage(j)
374    }
375}
376
377// ---------------------------------------------------------------------------
378// Solar Cell — Shockley-Queisser Limit & p-n Junction
379// ---------------------------------------------------------------------------
380
381/// Solar cell parameters (single-junction diode model).
382#[derive(Clone, Debug)]
383pub struct SolarCell {
384    /// Short-circuit current density Jsc (A/m²).
385    pub j_sc: f64,
386    /// Reverse saturation current density J0 (A/m²).
387    pub j0: f64,
388    /// Ideality factor n (1..2).
389    pub ideality: f64,
390    /// Series resistance (Ohm·m²).
391    pub r_series: f64,
392    /// Shunt resistance (Ohm·m²).
393    pub r_shunt: f64,
394    /// Temperature (K).
395    pub temperature: f64,
396    /// Bandgap energy Eg (eV).
397    pub bandgap_ev: f64,
398}
399
400impl SolarCell {
401    /// Create a new solar cell with given parameters.
402    ///
403    /// # Arguments
404    /// * `j_sc` – Short-circuit current density (A/m²)
405    /// * `j0` – Reverse saturation current density (A/m²)
406    /// * `ideality` – Ideality factor (1–2)
407    /// * `temperature` – Temperature (K)
408    /// * `r_series` – Series resistance (Ω·m²)
409    pub fn new(j_sc: f64, j0: f64, ideality: f64, temperature: f64, r_series: f64) -> Self {
410        Self {
411            j_sc,
412            j0,
413            ideality,
414            r_series,
415            r_shunt: 1.0e4,
416            temperature,
417            bandgap_ev: 1.12,
418        }
419    }
420
421    /// Typical silicon solar cell (AM1.5 illumination).
422    pub fn silicon() -> Self {
423        Self {
424            j_sc: 400.0,
425            j0: 1.0e-10,
426            ideality: 1.0,
427            r_series: 1.0e-5,
428            r_shunt: 1.0e3,
429            temperature: 298.15,
430            bandgap_ev: 1.12,
431        }
432    }
433
434    /// GaAs solar cell.
435    pub fn gaas() -> Self {
436        Self {
437            j_sc: 290.0,
438            j0: 1.0e-15,
439            ideality: 1.0,
440            r_series: 5.0e-6,
441            r_shunt: 5.0e3,
442            temperature: 298.15,
443            bandgap_ev: 1.42,
444        }
445    }
446
447    /// Thermal voltage Vt = kT/q (V).
448    pub fn thermal_voltage(&self) -> f64 {
449        K_B * self.temperature / Q_E
450    }
451
452    /// Diode current density (A/m²) at voltage V (including parasitic resistances).
453    pub fn current_density(&self, v: f64) -> f64 {
454        let vt = self.thermal_voltage() * self.ideality;
455        // Implicit equation; approximate solution ignoring r_series coupling
456        let j_diode = self.j0 * ((v / vt).exp() - 1.0);
457        let j_shunt = v / self.r_shunt;
458        self.j_sc - j_diode - j_shunt
459    }
460
461    /// Open circuit voltage Voc (V).
462    pub fn voc(&self) -> f64 {
463        let vt = self.thermal_voltage() * self.ideality;
464        vt * (self.j_sc / self.j0 + 1.0).ln()
465    }
466
467    /// Estimate fill factor (FF) using Ro = Jsc * Voc / J0-normalised formula.
468    pub fn fill_factor(&self) -> f64 {
469        let voc_norm = self.voc() / (self.thermal_voltage() * self.ideality);
470        // Empirical formula (Green, 1982)
471        (voc_norm - (voc_norm + 0.72).ln()) / (voc_norm + 1.0)
472    }
473
474    /// Maximum power point voltage Vmpp (V).
475    pub fn v_mpp(&self) -> f64 {
476        let voc = self.voc();
477        let vt = self.thermal_voltage() * self.ideality;
478        // Approximate: Vmpp ≈ Voc - Vt * ln(Vmpp / Vt + 1)
479        // Iterate
480        let mut v = voc * 0.8;
481        for _ in 0..20 {
482            v = voc - vt * (v / vt + 1.0).ln();
483        }
484        v
485    }
486
487    /// Maximum power density (W/m²).
488    pub fn p_max(&self) -> f64 {
489        let vmpp = self.v_mpp();
490        let jmpp = self.current_density(vmpp);
491        vmpp * jmpp.max(0.0)
492    }
493
494    /// Power conversion efficiency at AM1.5 (1000 W/m² irradiance).
495    pub fn efficiency(&self, irradiance: f64) -> f64 {
496        self.p_max() / irradiance
497    }
498
499    /// Shockley-Queisser detailed balance limit efficiency for bandgap Eg (eV).
500    ///
501    /// Uses a simplified analytical approximation.
502    pub fn sq_limit(bandgap_ev: f64) -> f64 {
503        // Approximate SQ limit (rough numerical estimate)
504        let eg = bandgap_ev;
505        if !(0.5..=4.0).contains(&eg) {
506            return 0.0;
507        }
508        // Empirical curve fit
509
510        0.31 * (1.0 - ((eg - 1.34) / 0.8).powi(2)).max(0.0)
511    }
512
513    /// Temperature coefficient of Voc (V/K) — typically negative.
514    pub fn temp_coeff_voc(&self) -> f64 {
515        -K_B / Q_E * (self.j_sc / self.j0).ln() / self.temperature
516    }
517}
518
519// ---------------------------------------------------------------------------
520// Thermoelectrics — Seebeck, Peltier, Thomson, ZT Figure of Merit
521// ---------------------------------------------------------------------------
522
523/// Thermoelectric material properties.
524#[derive(Clone, Debug)]
525pub struct Thermoelectric {
526    /// Seebeck coefficient S (V/K).
527    pub seebeck: f64,
528    /// Electrical conductivity sigma (S/m).
529    pub electrical_conductivity: f64,
530    /// Thermal conductivity kappa (W/m·K).
531    pub thermal_conductivity: f64,
532    /// Operating temperature T (K).
533    pub temperature: f64,
534}
535
536impl Thermoelectric {
537    /// Bismuth telluride Bi₂Te₃ at room temperature.
538    pub fn bi2te3() -> Self {
539        Self {
540            seebeck: 200.0e-6,
541            electrical_conductivity: 1.0e5,
542            thermal_conductivity: 1.0,
543            temperature: 300.0,
544        }
545    }
546
547    /// PbTe at high temperature.
548    pub fn pbte_high_temp() -> Self {
549        Self {
550            seebeck: 300.0e-6,
551            electrical_conductivity: 7.0e4,
552            thermal_conductivity: 1.5,
553            temperature: 750.0,
554        }
555    }
556
557    /// Dimensionless figure of merit ZT = S² * sigma * T / kappa.
558    pub fn zt(&self) -> f64 {
559        self.seebeck * self.seebeck * self.electrical_conductivity * self.temperature
560            / self.thermal_conductivity
561    }
562
563    /// Power factor PF = S² * sigma (W/m·K²).
564    pub fn power_factor(&self) -> f64 {
565        self.seebeck * self.seebeck * self.electrical_conductivity
566    }
567
568    /// Carnot efficiency limit (dimensionless).
569    pub fn carnot_efficiency(&self, t_cold: f64) -> f64 {
570        1.0 - t_cold / self.temperature
571    }
572
573    /// Maximum thermoelectric efficiency (generator mode).
574    pub fn max_efficiency(&self, t_hot: f64, t_cold: f64) -> f64 {
575        let zt_avg = self.zt(); // Using T_hot as reference
576        let sqrt_term = (1.0 + zt_avg).sqrt();
577        let delta_t = t_hot - t_cold;
578        (delta_t / t_hot) * (sqrt_term - 1.0) / (sqrt_term + t_cold / t_hot)
579    }
580
581    /// Peltier coefficient Pi = S * T (W/A = V).
582    pub fn peltier_coefficient(&self) -> f64 {
583        self.seebeck * self.temperature
584    }
585
586    /// Thomson coefficient tau = T * dS/dT (V/K).
587    ///
588    /// Approximated using finite difference of S over a small dT.
589    pub fn thomson_coefficient(&self, ds_dt: f64) -> f64 {
590        self.temperature * ds_dt
591    }
592
593    /// Maximum COP for Peltier cooler.
594    pub fn max_cop_cooling(&self, t_hot: f64, t_cold: f64) -> f64 {
595        let zt_avg = self.zt();
596        let sqrt_term = (1.0 + zt_avg).sqrt();
597        let delta_t = t_hot - t_cold;
598        (t_cold * (sqrt_term - t_hot / t_cold)) / (delta_t * (sqrt_term + 1.0))
599    }
600
601    /// Open circuit voltage for temperature difference dt.
602    pub fn seebeck_voltage(&self, dt: f64) -> f64 {
603        self.seebeck * dt
604    }
605
606    /// Thermal resistance (K·m/W) per unit cross-sectional area and length.
607    pub fn thermal_resistance(&self, length: f64) -> f64 {
608        length / self.thermal_conductivity
609    }
610}
611
612// ---------------------------------------------------------------------------
613// Supercapacitor — EDL and Pseudocapacitance
614// ---------------------------------------------------------------------------
615
616/// Electrochemical double layer (EDL) supercapacitor model.
617#[derive(Clone, Debug)]
618pub struct Supercapacitor {
619    /// Specific capacitance per unit area (F/m²).
620    pub c_specific: f64,
621    /// Electrode surface area (m²).
622    pub electrode_area: f64,
623    /// Electrolyte resistance (Ohm).
624    pub r_esr: f64,
625    /// Leakage resistance (Ohm).
626    pub r_leakage: f64,
627    /// Maximum voltage (V).
628    pub v_max: f64,
629    /// Pseudocapacitance fraction (0..1).
630    pub pseudo_fraction: f64,
631    /// Temperature (K).
632    pub temperature: f64,
633}
634
635impl Supercapacitor {
636    /// Activated carbon EDL supercapacitor (500 F).
637    pub fn activated_carbon_500f() -> Self {
638        Self {
639            c_specific: 0.2,
640            electrode_area: 2500.0,
641            r_esr: 1.0e-3,
642            r_leakage: 1.0e5,
643            v_max: 2.7,
644            pseudo_fraction: 0.0,
645            temperature: 298.15,
646        }
647    }
648
649    /// RuO₂ pseudocapacitor.
650    pub fn ruo2_pseudocap() -> Self {
651        Self {
652            c_specific: 0.8,
653            electrode_area: 500.0,
654            r_esr: 5.0e-3,
655            r_leakage: 5.0e4,
656            v_max: 1.2,
657            pseudo_fraction: 0.6,
658            temperature: 298.15,
659        }
660    }
661
662    /// Total capacitance (F).
663    pub fn capacitance(&self) -> f64 {
664        self.c_specific * self.electrode_area * (1.0 + self.pseudo_fraction)
665    }
666
667    /// Stored energy (J) at voltage V.
668    pub fn energy_stored(&self, v: f64) -> f64 {
669        0.5 * self.capacitance() * v * v
670    }
671
672    /// Maximum stored energy (J).
673    pub fn max_energy(&self) -> f64 {
674        self.energy_stored(self.v_max)
675    }
676
677    /// Peak power (W) limited by ESR.
678    pub fn peak_power(&self) -> f64 {
679        self.v_max * self.v_max / (4.0 * self.r_esr)
680    }
681
682    /// Specific energy (Wh/kg) assuming electrode mass `m_kg`.
683    pub fn specific_energy_wh_kg(&self, m_kg: f64) -> f64 {
684        self.max_energy() / (m_kg * 3600.0)
685    }
686
687    /// RC time constant (s).
688    pub fn time_constant(&self) -> f64 {
689        self.r_esr * self.capacitance()
690    }
691
692    /// Voltage decay under constant leakage: V(t) = V0 * exp(-t / (R_leak * C)).
693    pub fn voltage_decay(&self, v0: f64, t: f64) -> f64 {
694        let tau_leak = self.r_leakage * self.capacitance();
695        v0 * (-t / tau_leak).exp()
696    }
697
698    /// Charge stored (C) at voltage V.
699    pub fn charge_stored(&self, v: f64) -> f64 {
700        self.capacitance() * v
701    }
702
703    /// Helmholtz EDL capacitance per area (F/m²) from Stern model.
704    pub fn helmholtz_capacitance(epsilon_r: f64, d_debye: f64) -> f64 {
705        8.854187817e-12 * epsilon_r / d_debye
706    }
707
708    /// Debye screening length (m).
709    pub fn debye_length(c_mol_l: f64, z: f64, temperature: f64) -> f64 {
710        let c_mol_m3 = c_mol_l * 1000.0;
711        let eps = 8.854e-12 * 80.0; // Water dielectric constant
712        (eps * R * temperature / (2.0 * F_FARADAY * F_FARADAY * c_mol_m3 * z * z)).sqrt()
713    }
714}
715
716// ---------------------------------------------------------------------------
717// Hydrogen Storage — Metal Hydride Kinetics
718// ---------------------------------------------------------------------------
719
720/// Metal hydride hydrogen storage model.
721#[derive(Clone, Debug)]
722pub struct MetalHydride {
723    /// Maximum hydrogen storage capacity (wt%).
724    pub max_capacity_wt: f64,
725    /// Activation energy for absorption Ea (J/mol).
726    pub ea_absorption: f64,
727    /// Activation energy for desorption Ea (J/mol).
728    pub ea_desorption: f64,
729    /// Pre-exponential factor ka (1/s).
730    pub k0_absorption: f64,
731    /// Pre-exponential factor kd (1/s).
732    pub k0_desorption: f64,
733    /// Equilibrium plateau pressure at 298 K (Pa).
734    pub p_eq_298: f64,
735    /// Enthalpy of hydride formation Delta_H (J/mol H₂).
736    pub delta_h: f64,
737    /// Entropy of hydride formation Delta_S (J/mol H₂/K).
738    pub delta_s: f64,
739    /// Temperature (K).
740    pub temperature: f64,
741}
742
743impl MetalHydride {
744    /// LaNi₅ hydride defaults.
745    pub fn lani5() -> Self {
746        Self {
747            max_capacity_wt: 1.4,
748            ea_absorption: 21_000.0,
749            ea_desorption: 35_000.0,
750            k0_absorption: 500.0,
751            k0_desorption: 200.0,
752            p_eq_298: 200_000.0,
753            delta_h: -30_800.0,
754            delta_s: -108.0,
755            temperature: 298.15,
756        }
757    }
758
759    /// MgH₂ high-capacity hydride.
760    pub fn mgh2() -> Self {
761        Self {
762            max_capacity_wt: 7.6,
763            ea_absorption: 60_000.0,
764            ea_desorption: 80_000.0,
765            k0_absorption: 100.0,
766            k0_desorption: 50.0,
767            p_eq_298: 0.01,
768            delta_h: -75_000.0,
769            delta_s: -135.0,
770            temperature: 623.15,
771        }
772    }
773
774    /// Van't Hoff equilibrium pressure at temperature T (Pa).
775    pub fn equilibrium_pressure(&self, t: f64) -> f64 {
776        let p0 = 1.0e5; // Reference pressure (Pa)
777        p0 * (self.delta_h / (R * t) - self.delta_s / R).exp()
778    }
779
780    /// Absorption rate constant (1/s) at temperature T.
781    pub fn absorption_rate_constant(&self, t: f64) -> f64 {
782        self.k0_absorption * (-self.ea_absorption / (R * t)).exp()
783    }
784
785    /// Desorption rate constant (1/s) at temperature T.
786    pub fn desorption_rate_constant(&self, t: f64) -> f64 {
787        self.k0_desorption * (-self.ea_desorption / (R * t)).exp()
788    }
789
790    /// Reaction kinetics (d\[H\]/dt) (wt%/s) during absorption.
791    ///
792    /// `x`: current H content (wt%). `p`: applied H₂ pressure (Pa).
793    pub fn absorption_rate(&self, x: f64, p: f64) -> f64 {
794        let peq = self.equilibrium_pressure(self.temperature);
795        let ka = self.absorption_rate_constant(self.temperature);
796        if p <= peq {
797            return 0.0;
798        }
799        ka * (self.max_capacity_wt - x) * ((p / peq).ln()).max(0.0)
800    }
801
802    /// Reaction kinetics (d\[H\]/dt) (wt%/s) during desorption.
803    pub fn desorption_rate(&self, x: f64, p: f64) -> f64 {
804        let peq = self.equilibrium_pressure(self.temperature);
805        let kd = self.desorption_rate_constant(self.temperature);
806        if p >= peq {
807            return 0.0;
808        }
809        kd * x * ((peq / p.max(1e-10)).ln()).max(0.0)
810    }
811
812    /// Heat generated during absorption (W/kg H₂).
813    pub fn absorption_heat(&self, rate_wt_per_s: f64) -> f64 {
814        // Q = -Delta_H / M_H2 * rate
815        rate_wt_per_s * (-self.delta_h) / 2.016 // kJ/kg approximate
816    }
817}
818
819// ---------------------------------------------------------------------------
820// Nuclear Materials — Radiation Damage and Swelling
821// ---------------------------------------------------------------------------
822
823/// Nuclear material radiation damage model.
824#[derive(Clone, Debug)]
825pub struct NuclearMaterial {
826    /// Material name.
827    pub name: String,
828    /// Threshold displacement energy Ed (eV).
829    pub e_displacement: f64,
830    /// Swelling rate (%/dpa) at operating temperature.
831    pub swelling_rate: f64,
832    /// Irradiation hardening coefficient (MPa / sqrt(dpa)).
833    pub hardening_coeff: f64,
834    /// Creep compliance coefficient B (per Pa per dpa).
835    pub creep_coefficient: f64,
836    /// Embrittlement transition temperature shift (K/dpa).
837    pub dbtt_shift_per_dpa: f64,
838    /// Initial yield stress (Pa).
839    pub yield_stress_0: f64,
840    /// Young's modulus (Pa).
841    pub modulus: f64,
842}
843
844impl NuclearMaterial {
845    /// Ferritic-martensitic steel (F82H) for fusion applications.
846    pub fn f82h() -> Self {
847        Self {
848            name: "F82H ferritic-martensitic steel".into(),
849            e_displacement: 40.0,
850            swelling_rate: 0.002,
851            hardening_coeff: 120.0e6,
852            creep_coefficient: 1.0e-6,
853            dbtt_shift_per_dpa: 1.5,
854            yield_stress_0: 500.0e6,
855            modulus: 210.0e9,
856        }
857    }
858
859    /// Austenitic stainless steel 316L.
860    pub fn ss316l() -> Self {
861        Self {
862            name: "316L stainless steel".into(),
863            e_displacement: 40.0,
864            swelling_rate: 0.05,
865            hardening_coeff: 200.0e6,
866            creep_coefficient: 2.0e-6,
867            dbtt_shift_per_dpa: 0.0,
868            yield_stress_0: 270.0e6,
869            modulus: 193.0e9,
870        }
871    }
872
873    /// Displacement damage (dpa) from neutron fluence.
874    ///
875    /// Uses the NRT (Norgett-Robinson-Torrens) standard.
876    pub fn dpa_from_fluence(&self, fluence: f64, sigma_d: f64) -> f64 {
877        // dpa = phi * sigma_d * t (simplified)
878        fluence * sigma_d
879    }
880
881    /// Radiation-induced swelling (volume fraction).
882    pub fn swelling(&self, dpa: f64) -> f64 {
883        // Incubation period (~1 dpa) then linear
884        if dpa < 1.0 {
885            return 0.0;
886        }
887        self.swelling_rate * (dpa - 1.0) / 100.0
888    }
889
890    /// Irradiation hardening: delta_sigma_y = k * sqrt(dpa).
891    pub fn irradiation_hardening(&self, dpa: f64) -> f64 {
892        self.hardening_coeff * dpa.sqrt()
893    }
894
895    /// Yield stress after irradiation.
896    pub fn irradiated_yield_stress(&self, dpa: f64) -> f64 {
897        self.yield_stress_0 + self.irradiation_hardening(dpa)
898    }
899
900    /// Irradiation creep strain rate (1/s) under stress sigma and flux phi (dpa/s).
901    pub fn irradiation_creep_rate(&self, sigma: f64, dpas_per_s: f64) -> f64 {
902        self.creep_coefficient * sigma * dpas_per_s
903    }
904
905    /// Ductile-to-brittle transition temperature shift (K).
906    pub fn dbtt_shift(&self, dpa: f64) -> f64 {
907        self.dbtt_shift_per_dpa * dpa
908    }
909
910    /// PKA (primary knock-on atom) energy from Lindhard theory.
911    ///
912    /// `e_recoil`: recoil energy (eV). Returns effective PKA energy (eV).
913    pub fn pka_lindhard(&self, e_recoil: f64, atomic_mass: f64) -> f64 {
914        let xi = 0.16 * atomic_mass.powf(-2.0 / 3.0);
915        e_recoil / (1.0 + xi * e_recoil / self.e_displacement)
916    }
917
918    /// NRT displacement cross-section (barn, simplified).
919    pub fn nrt_displacements(&self, e_pka: f64) -> f64 {
920        if e_pka < self.e_displacement {
921            return 0.0;
922        }
923        0.8 * e_pka / (2.0 * self.e_displacement)
924    }
925}
926
927// ---------------------------------------------------------------------------
928// Piezoelectric Energy Harvesting
929// ---------------------------------------------------------------------------
930
931/// Piezoelectric energy harvesting model (bimorph cantilever).
932#[derive(Clone, Debug)]
933pub struct PiezoHarvester {
934    /// Piezoelectric coupling coefficient d₃₁ (C/N = m/V).
935    pub d31: f64,
936    /// Electromechanical coupling factor k₃₁ (dimensionless).
937    pub k31: f64,
938    /// Young's modulus of piezo material (Pa).
939    pub e_piezo: f64,
940    /// Density of piezo material (kg/m³).
941    pub density: f64,
942    /// Beam length L (m).
943    pub length: f64,
944    /// Beam width b (m).
945    pub width: f64,
946    /// Beam thickness h (m).
947    pub thickness: f64,
948    /// Permittivity at constant stress epsilon_T (F/m).
949    pub permittivity: f64,
950    /// Mechanical damping ratio zeta.
951    pub damping: f64,
952    /// Proof mass (kg).
953    pub proof_mass: f64,
954}
955
956impl PiezoHarvester {
957    /// PZT-5A bimorph cantilever defaults.
958    pub fn pzt5a() -> Self {
959        Self {
960            d31: -175.0e-12,
961            k31: 0.34,
962            e_piezo: 61.0e9,
963            density: 7750.0,
964            length: 30.0e-3,
965            width: 5.0e-3,
966            thickness: 0.5e-3,
967            permittivity: 15.0e-9,
968            damping: 0.02,
969            proof_mass: 1.0e-3,
970        }
971    }
972
973    /// Natural (resonant) frequency f_n (Hz) for a bimorph cantilever.
974    pub fn natural_frequency(&self) -> f64 {
975        let ei = self.e_piezo * self.width * self.thickness.powi(3) / 12.0;
976        let m = self.proof_mass + 0.23 * self.density * self.width * self.thickness * self.length;
977        let k = 3.0 * ei / self.length.powi(3);
978        (k / m).sqrt() / (2.0 * PI)
979    }
980
981    /// Open circuit voltage amplitude V_oc (V) at base acceleration a (m/s²) and excitation freq f.
982    pub fn open_circuit_voltage(&self, a: f64, f: f64) -> f64 {
983        let fn_ = self.natural_frequency();
984        let omega = 2.0 * PI * f;
985        let omega_n = 2.0 * PI * fn_;
986        let r = omega / omega_n;
987        let m = self.proof_mass;
988        // Tip displacement amplitude
989        let denom = ((1.0 - r * r).powi(2) + (2.0 * self.damping * r).powi(2)).sqrt();
990        let x_amp =
991            m * a / (self.e_piezo * self.width * self.thickness * denom / self.length.powi(2));
992        // Voltage from piezoelectric coupling
993        let h_pc = self.e_piezo * self.d31.abs() * 3.0 * self.thickness
994            / (2.0 * self.length * self.length);
995        let c_p = self.permittivity * self.width * self.length / self.thickness;
996        h_pc * x_amp / c_p
997    }
998
999    /// Maximum power output (W) at resonance into matched load.
1000    pub fn max_power(&self, a: f64) -> f64 {
1001        let fn_ = self.natural_frequency();
1002        let voc = self.open_circuit_voltage(a, fn_);
1003        let c_p = self.permittivity * self.width * self.length / self.thickness;
1004        let omega_n = 2.0 * PI * fn_;
1005        let r_opt = 1.0 / (omega_n * c_p);
1006        voc * voc / (4.0 * r_opt)
1007    }
1008
1009    /// Normalised power density (W/m³) for volume Vol.
1010    pub fn power_density(&self, a: f64) -> f64 {
1011        let vol = self.length * self.width * self.thickness;
1012        self.max_power(a) / vol
1013    }
1014
1015    /// Effective piezoelectric coefficient (C/m²) g₃₁ = d₃₁ / epsilon_T.
1016    pub fn g31(&self) -> f64 {
1017        self.d31 / self.permittivity
1018    }
1019
1020    /// Mechanical quality factor Qm ≈ 1 / (2 * zeta).
1021    pub fn quality_factor(&self) -> f64 {
1022        1.0 / (2.0 * self.damping)
1023    }
1024
1025    /// Efficiency of electromechanical coupling: k²/(1+k²), in \[0,1).
1026    pub fn coupling_efficiency(&self) -> f64 {
1027        let k2 = self.k31 * self.k31;
1028        k2 / (1.0 + k2)
1029    }
1030}
1031
1032// ---------------------------------------------------------------------------
1033// Electrode material capacity and voltage models
1034// ---------------------------------------------------------------------------
1035
1036/// Capacity fading model for Li-ion batteries under cycling.
1037#[derive(Clone, Debug)]
1038pub struct CapacityFadingModel {
1039    /// Initial capacity (A·h).
1040    pub q0: f64,
1041    /// SEI growth rate constant k_sei (s⁻¹·⁰·⁵).
1042    pub k_sei: f64,
1043    /// Calendar aging coefficient k_cal.
1044    pub k_cal: f64,
1045    /// Cycle aging exponent.
1046    pub cycle_exp: f64,
1047}
1048
1049impl CapacityFadingModel {
1050    /// Typical NMC 18650 cell aging model.
1051    pub fn nmc18650() -> Self {
1052        Self {
1053            q0: 3.0,
1054            k_sei: 1.5e-4,
1055            k_cal: 1.2e-4,
1056            cycle_exp: 0.5,
1057        }
1058    }
1059
1060    /// Capacity after `n_cycles` under standard cycling conditions.
1061    pub fn capacity_after_cycles(&self, n_cycles: f64) -> f64 {
1062        self.q0 * (1.0 - self.k_cal * n_cycles.powf(self.cycle_exp))
1063    }
1064
1065    /// Capacity after time `t_hours` of calendar aging.
1066    pub fn capacity_calendar(&self, t_hours: f64) -> f64 {
1067        self.q0 * (1.0 - self.k_sei * t_hours.sqrt())
1068    }
1069
1070    /// State of health (SOH) from current capacity.
1071    pub fn state_of_health(&self, q_current: f64) -> f64 {
1072        q_current / self.q0
1073    }
1074
1075    /// Cycle life (number of cycles to 80% SOH).
1076    pub fn cycle_life_80pct(&self) -> f64 {
1077        let fade_target = 0.20;
1078        (fade_target / self.k_cal).powf(1.0 / self.cycle_exp)
1079    }
1080}
1081
1082// ---------------------------------------------------------------------------
1083// Tests
1084// ---------------------------------------------------------------------------
1085
1086#[cfg(test)]
1087mod tests {
1088    use super::*;
1089
1090    fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
1091        (a - b).abs() < tol
1092    }
1093
1094    // --- LiIonElectrode ---
1095
1096    #[test]
1097    fn test_butler_volmer_zero_eta() {
1098        let e = LiIonElectrode::lco_cathode();
1099        let i = e.butler_volmer(0.0);
1100        assert!(i.abs() < 1e-10, "BV at zero overpotential should be ~0");
1101    }
1102
1103    #[test]
1104    fn test_butler_volmer_positive_eta() {
1105        let e = LiIonElectrode::lco_cathode();
1106        let i = e.butler_volmer(0.05);
1107        assert!(
1108            i > 0.0,
1109            "Positive overpotential should give positive current"
1110        );
1111    }
1112
1113    #[test]
1114    fn test_diffusion_time_constant() {
1115        let e = LiIonElectrode::graphite_anode();
1116        let tau = e.diffusion_time_constant();
1117        assert!(tau > 0.0);
1118        // tau = R^2 / (15 * D) = (5e-6)^2 / (15 * 3.9e-14) ≈ 0.04 s
1119        let expected = 5.0e-6_f64.powi(2) / (15.0 * 3.9e-14);
1120        assert!(approx_eq(tau, expected, 1e-5));
1121    }
1122
1123    #[test]
1124    fn test_areal_capacity_positive() {
1125        let e = LiIonElectrode::lco_cathode();
1126        let q = e.areal_capacity();
1127        assert!(q > 0.0);
1128    }
1129
1130    #[test]
1131    fn test_tafel_slope_positive() {
1132        let e = LiIonElectrode::lco_cathode();
1133        assert!(e.tafel_slope() > 0.0);
1134    }
1135
1136    // --- NafionMembrane ---
1137
1138    #[test]
1139    fn test_nafion_conductivity_positive() {
1140        let m = NafionMembrane::nafion117();
1141        let sigma = m.proton_conductivity();
1142        assert!(sigma > 0.0);
1143    }
1144
1145    #[test]
1146    fn test_nafion_asr_positive() {
1147        let m = NafionMembrane::nafion117();
1148        let asr = m.area_specific_resistance();
1149        assert!(asr > 0.0);
1150    }
1151
1152    #[test]
1153    fn test_nafion_sorption_isotherm() {
1154        let m = NafionMembrane::nafion117();
1155        let lambda_0 = m.sorption_isotherm(0.0);
1156        let lambda_1 = m.sorption_isotherm(1.0);
1157        assert!(lambda_0 >= 0.0);
1158        assert!(lambda_1 > lambda_0);
1159    }
1160
1161    // --- GasDiffusionLayer ---
1162
1163    #[test]
1164    fn test_gdl_effective_diffusivity() {
1165        let gdl = GasDiffusionLayer::toray_tgp_h120();
1166        let d_eff = gdl.effective_diffusivity(2.6e-5);
1167        assert!(d_eff < 2.6e-5 && d_eff > 0.0);
1168    }
1169
1170    #[test]
1171    fn test_gdl_darcy_velocity() {
1172        let gdl = GasDiffusionLayer::toray_tgp_h120();
1173        let v = gdl.darcy_velocity(1e-5, 1000.0);
1174        assert!(v > 0.0);
1175    }
1176
1177    // --- SolarCell ---
1178
1179    #[test]
1180    fn test_solar_cell_voc_positive() {
1181        let cell = SolarCell::silicon();
1182        let voc = cell.voc();
1183        assert!(voc > 0.5 && voc < 1.0);
1184    }
1185
1186    #[test]
1187    fn test_solar_cell_fill_factor_range() {
1188        let cell = SolarCell::silicon();
1189        let ff = cell.fill_factor();
1190        assert!(ff > 0.5 && ff < 0.99);
1191    }
1192
1193    #[test]
1194    fn test_solar_cell_efficiency() {
1195        let cell = SolarCell::silicon();
1196        let eta = cell.efficiency(1000.0);
1197        assert!(eta > 0.0 && eta < 0.5);
1198    }
1199
1200    #[test]
1201    fn test_sq_limit_silicon() {
1202        let eta = SolarCell::sq_limit(1.12);
1203        assert!(eta > 0.0);
1204    }
1205
1206    #[test]
1207    fn test_solar_temp_coeff_negative() {
1208        let cell = SolarCell::silicon();
1209        assert!(cell.temp_coeff_voc() < 0.0);
1210    }
1211
1212    // --- Thermoelectric ---
1213
1214    #[test]
1215    fn test_zt_bi2te3() {
1216        let te = Thermoelectric::bi2te3();
1217        let zt = te.zt();
1218        // Bi2Te3 is state-of-the-art: ZT ~ 1
1219        assert!(zt > 0.5 && zt < 2.0);
1220    }
1221
1222    #[test]
1223    fn test_power_factor_positive() {
1224        let te = Thermoelectric::bi2te3();
1225        assert!(te.power_factor() > 0.0);
1226    }
1227
1228    #[test]
1229    fn test_peltier_coefficient() {
1230        let te = Thermoelectric::bi2te3();
1231        let pi = te.peltier_coefficient();
1232        assert!(approx_eq(pi, te.seebeck * te.temperature, 1e-20));
1233    }
1234
1235    #[test]
1236    fn test_seebeck_voltage() {
1237        let te = Thermoelectric::bi2te3();
1238        let v = te.seebeck_voltage(10.0);
1239        assert!(approx_eq(v, te.seebeck * 10.0, 1e-20));
1240    }
1241
1242    #[test]
1243    fn test_max_efficiency_positive() {
1244        let te = Thermoelectric::bi2te3();
1245        let eta = te.max_efficiency(350.0, 250.0);
1246        assert!(eta > 0.0 && eta < 1.0);
1247    }
1248
1249    // --- Supercapacitor ---
1250
1251    #[test]
1252    fn test_supercap_capacitance() {
1253        let sc = Supercapacitor::activated_carbon_500f();
1254        let c = sc.capacitance();
1255        assert!(approx_eq(c, sc.c_specific * sc.electrode_area, 1.0));
1256    }
1257
1258    #[test]
1259    fn test_supercap_energy_stored() {
1260        let sc = Supercapacitor::activated_carbon_500f();
1261        let e = sc.energy_stored(1.0);
1262        assert!(approx_eq(e, 0.5 * sc.capacitance() * 1.0, 1.0));
1263    }
1264
1265    #[test]
1266    fn test_supercap_peak_power() {
1267        let sc = Supercapacitor::activated_carbon_500f();
1268        let p = sc.peak_power();
1269        assert!(p > 0.0);
1270    }
1271
1272    #[test]
1273    fn test_supercap_voltage_decay() {
1274        let sc = Supercapacitor::activated_carbon_500f();
1275        let v0 = 2.7;
1276        let v1 = sc.voltage_decay(v0, 3600.0);
1277        assert!(v1 < v0);
1278    }
1279
1280    #[test]
1281    fn test_debye_length_dilute() {
1282        let ld = Supercapacitor::debye_length(0.001, 1.0, 298.15);
1283        // ~10 nm for 1 mM
1284        assert!(ld > 1e-9 && ld < 1e-6);
1285    }
1286
1287    // --- MetalHydride ---
1288
1289    #[test]
1290    fn test_lani5_eq_pressure() {
1291        let mh = MetalHydride::lani5();
1292        let peq = mh.equilibrium_pressure(298.15);
1293        assert!(peq > 0.0);
1294    }
1295
1296    #[test]
1297    fn test_absorption_rate_zero_below_eq() {
1298        let mh = MetalHydride::lani5();
1299        let peq = mh.equilibrium_pressure(mh.temperature);
1300        let rate = mh.absorption_rate(0.0, peq * 0.5);
1301        assert!(rate == 0.0);
1302    }
1303
1304    #[test]
1305    fn test_absorption_rate_positive_above_eq() {
1306        let mh = MetalHydride::lani5();
1307        let peq = mh.equilibrium_pressure(mh.temperature);
1308        let rate = mh.absorption_rate(0.0, peq * 2.0);
1309        assert!(rate > 0.0);
1310    }
1311
1312    // --- NuclearMaterial ---
1313
1314    #[test]
1315    fn test_swelling_zero_at_low_dpa() {
1316        let nm = NuclearMaterial::f82h();
1317        assert_eq!(nm.swelling(0.5), 0.0);
1318    }
1319
1320    #[test]
1321    fn test_swelling_positive_above_threshold() {
1322        let nm = NuclearMaterial::f82h();
1323        let sw = nm.swelling(10.0);
1324        assert!(sw > 0.0);
1325    }
1326
1327    #[test]
1328    fn test_irradiation_hardening_increases() {
1329        let nm = NuclearMaterial::f82h();
1330        let h1 = nm.irradiation_hardening(1.0);
1331        let h10 = nm.irradiation_hardening(10.0);
1332        assert!(h10 > h1);
1333    }
1334
1335    #[test]
1336    fn test_irradiated_yield_stress() {
1337        let nm = NuclearMaterial::ss316l();
1338        let sy = nm.irradiated_yield_stress(5.0);
1339        assert!(sy > nm.yield_stress_0);
1340    }
1341
1342    #[test]
1343    fn test_nrt_displacements_zero_below_threshold() {
1344        let nm = NuclearMaterial::f82h();
1345        assert_eq!(nm.nrt_displacements(nm.e_displacement - 1.0), 0.0);
1346    }
1347
1348    // --- PiezoHarvester ---
1349
1350    #[test]
1351    fn test_piezo_natural_frequency() {
1352        let ph = PiezoHarvester::pzt5a();
1353        let fn_ = ph.natural_frequency();
1354        assert!(fn_ > 0.0 && fn_ < 10000.0);
1355    }
1356
1357    #[test]
1358    fn test_piezo_quality_factor() {
1359        let ph = PiezoHarvester::pzt5a();
1360        let qm = ph.quality_factor();
1361        assert!(approx_eq(qm, 25.0, 1e-10));
1362    }
1363
1364    #[test]
1365    fn test_piezo_coupling_efficiency() {
1366        let ph = PiezoHarvester::pzt5a();
1367        let eta = ph.coupling_efficiency();
1368        assert!(eta > 0.0 && eta < 1.0);
1369    }
1370
1371    // --- CapacityFadingModel ---
1372
1373    #[test]
1374    fn test_capacity_fading_decreases() {
1375        let m = CapacityFadingModel::nmc18650();
1376        let q100 = m.capacity_after_cycles(100.0);
1377        let q500 = m.capacity_after_cycles(500.0);
1378        assert!(q100 > q500);
1379    }
1380
1381    #[test]
1382    fn test_capacity_soh_at_start() {
1383        let m = CapacityFadingModel::nmc18650();
1384        let soh = m.state_of_health(m.q0);
1385        assert!(approx_eq(soh, 1.0, 1e-10));
1386    }
1387
1388    #[test]
1389    fn test_cycle_life_positive() {
1390        let m = CapacityFadingModel::nmc18650();
1391        let life = m.cycle_life_80pct();
1392        assert!(life > 0.0);
1393    }
1394
1395    // --- LiIonCell ---
1396
1397    #[test]
1398    fn test_cell_ocv_positive() {
1399        let cell = LiIonCell::nmc18650();
1400        let ocv = cell.ocv(0.5, 0.5);
1401        assert!(ocv.abs() < 5.0);
1402    }
1403
1404    #[test]
1405    fn test_pem_fuel_cell_voltage() {
1406        let fc = PemFuelCell::standard();
1407        let v = fc.cell_voltage(100.0);
1408        assert!((0.0..=1.3).contains(&v));
1409    }
1410
1411    #[test]
1412    fn test_pem_power_density() {
1413        let fc = PemFuelCell::standard();
1414        let p = fc.power_density(1000.0);
1415        assert!(p > 0.0);
1416    }
1417}