1#![allow(dead_code)]
11#![allow(unused_imports)]
12#![allow(clippy::too_many_arguments)]
13
14use std::f64::consts::PI;
15
16const R: f64 = 8.314;
18const F_FARADAY: f64 = 96_485.0;
20const K_B: f64 = 1.380649e-23;
22const Q_E: f64 = 1.602176634e-19;
24const H_PLANCK: f64 = 6.62607015e-34;
26const C_LIGHT: f64 = 2.99792458e8;
28
29#[derive(Clone, Debug)]
35pub struct LiIonElectrode {
36 pub i0: f64,
38 pub alpha: f64,
40 pub temperature: f64,
42 pub thickness: f64,
44 pub d_solid: f64,
46 pub d_electrolyte: f64,
48 pub cs_max: f64,
50 pub epsilon_s: f64,
52 pub particle_radius: f64,
54 pub ocp_a: f64,
56 pub ocp_b: f64,
58}
59
60impl LiIonElectrode {
61 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 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 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 pub fn linear_butler_volmer(&self, eta: f64) -> f64 {
103 self.i0 * F_FARADAY / (R * self.temperature) * eta
104 }
105
106 pub fn ocp(&self, x: f64) -> f64 {
108 self.ocp_a + self.ocp_b * x
109 }
110
111 pub fn diffusion_time_constant(&self) -> f64 {
113 self.particle_radius * self.particle_radius / (15.0 * self.d_solid)
114 }
115
116 pub fn specific_interfacial_area(&self) -> f64 {
118 3.0 * self.epsilon_s / self.particle_radius
119 }
120
121 pub fn areal_capacity(&self) -> f64 {
123 self.cs_max * F_FARADAY * self.epsilon_s * self.thickness
124 }
125
126 pub fn state_of_charge(&self, cs_surf: f64) -> f64 {
128 (cs_surf / self.cs_max).clamp(0.0, 1.0)
129 }
130
131 pub fn tafel_slope(&self) -> f64 {
133 R * self.temperature / (self.alpha * F_FARADAY) * 10.0f64.ln()
134 }
135}
136
137#[derive(Clone, Debug)]
139pub struct LiIonCell {
140 pub cathode: LiIonElectrode,
142 pub anode: LiIonElectrode,
144 pub r_electrolyte: f64,
146 pub r_contact: f64,
148 pub capacity_ah: f64,
150}
151
152impl LiIonCell {
153 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 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 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 pub fn c_rate(&self, current_a: f64) -> f64 {
180 current_a / self.capacity_ah
181 }
182
183 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; let power = current * voltage;
188 let energy = self.capacity_ah * voltage;
189 (energy / cell_mass_kg, power / cell_mass_kg)
190 }
191}
192
193#[derive(Clone, Debug)]
199pub struct NafionMembrane {
200 pub thickness: f64,
202 pub water_content: f64,
204 pub temperature: f64,
206 pub eq_weight: f64,
208 pub dry_density: f64,
210}
211
212impl NafionMembrane {
213 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 pub fn proton_conductivity(&self) -> f64 {
226 let lambda = self.water_content;
227 let t = self.temperature;
228 (0.005139 * lambda - 0.00326) * (1268.0 * (1.0 / 303.15 - 1.0 / t)).exp() * 1000.0 }
231
232 pub fn area_specific_resistance(&self) -> f64 {
234 self.thickness / self.proton_conductivity()
235 }
236
237 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 pub fn sorption_isotherm(&self, a_w: f64) -> f64 {
253 0.043 + 17.81 * a_w - 39.85 * a_w * a_w + 36.0 * a_w * a_w * a_w
255 }
256
257 pub fn electroosmotic_drag(&self) -> f64 {
259 2.5 * self.water_content / 22.0
260 }
261}
262
263#[derive(Clone, Debug)]
265pub struct GasDiffusionLayer {
266 pub porosity: f64,
268 pub tortuosity: f64,
270 pub thickness: f64,
272 pub contact_angle: f64,
274 pub permeability: f64,
276}
277
278impl GasDiffusionLayer {
279 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 pub fn effective_diffusivity(&self, d_bulk: f64) -> f64 {
292 d_bulk * self.porosity.powf(1.5)
293 }
294
295 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 pub fn darcy_velocity(&self, viscosity: f64, dp_dx: f64) -> f64 {
304 self.permeability / viscosity * dp_dx
305 }
306
307 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#[derive(Clone, Debug)]
315pub struct PemFuelCell {
316 pub membrane: NafionMembrane,
318 pub cathode_gdl: GasDiffusionLayer,
320 pub anode_gdl: GasDiffusionLayer,
322 pub i0_c: f64,
324 pub i0_a: f64,
326 pub alpha_c: f64,
328 pub e_nernst: f64,
330 pub temperature: f64,
332}
333
334impl PemFuelCell {
335 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 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 pub fn ohmic_overpotential(&self, j: f64) -> f64 {
356 j * self.membrane.area_specific_resistance()
357 }
358
359 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 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 pub fn power_density(&self, j: f64) -> f64 {
373 j * self.cell_voltage(j)
374 }
375}
376
377#[derive(Clone, Debug)]
383pub struct SolarCell {
384 pub j_sc: f64,
386 pub j0: f64,
388 pub ideality: f64,
390 pub r_series: f64,
392 pub r_shunt: f64,
394 pub temperature: f64,
396 pub bandgap_ev: f64,
398}
399
400impl SolarCell {
401 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 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 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 pub fn thermal_voltage(&self) -> f64 {
449 K_B * self.temperature / Q_E
450 }
451
452 pub fn current_density(&self, v: f64) -> f64 {
454 let vt = self.thermal_voltage() * self.ideality;
455 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 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 pub fn fill_factor(&self) -> f64 {
469 let voc_norm = self.voc() / (self.thermal_voltage() * self.ideality);
470 (voc_norm - (voc_norm + 0.72).ln()) / (voc_norm + 1.0)
472 }
473
474 pub fn v_mpp(&self) -> f64 {
476 let voc = self.voc();
477 let vt = self.thermal_voltage() * self.ideality;
478 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 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 pub fn efficiency(&self, irradiance: f64) -> f64 {
496 self.p_max() / irradiance
497 }
498
499 pub fn sq_limit(bandgap_ev: f64) -> f64 {
503 let eg = bandgap_ev;
505 if !(0.5..=4.0).contains(&eg) {
506 return 0.0;
507 }
508 0.31 * (1.0 - ((eg - 1.34) / 0.8).powi(2)).max(0.0)
511 }
512
513 pub fn temp_coeff_voc(&self) -> f64 {
515 -K_B / Q_E * (self.j_sc / self.j0).ln() / self.temperature
516 }
517}
518
519#[derive(Clone, Debug)]
525pub struct Thermoelectric {
526 pub seebeck: f64,
528 pub electrical_conductivity: f64,
530 pub thermal_conductivity: f64,
532 pub temperature: f64,
534}
535
536impl Thermoelectric {
537 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 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 pub fn zt(&self) -> f64 {
559 self.seebeck * self.seebeck * self.electrical_conductivity * self.temperature
560 / self.thermal_conductivity
561 }
562
563 pub fn power_factor(&self) -> f64 {
565 self.seebeck * self.seebeck * self.electrical_conductivity
566 }
567
568 pub fn carnot_efficiency(&self, t_cold: f64) -> f64 {
570 1.0 - t_cold / self.temperature
571 }
572
573 pub fn max_efficiency(&self, t_hot: f64, t_cold: f64) -> f64 {
575 let zt_avg = self.zt(); 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 pub fn peltier_coefficient(&self) -> f64 {
583 self.seebeck * self.temperature
584 }
585
586 pub fn thomson_coefficient(&self, ds_dt: f64) -> f64 {
590 self.temperature * ds_dt
591 }
592
593 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 pub fn seebeck_voltage(&self, dt: f64) -> f64 {
603 self.seebeck * dt
604 }
605
606 pub fn thermal_resistance(&self, length: f64) -> f64 {
608 length / self.thermal_conductivity
609 }
610}
611
612#[derive(Clone, Debug)]
618pub struct Supercapacitor {
619 pub c_specific: f64,
621 pub electrode_area: f64,
623 pub r_esr: f64,
625 pub r_leakage: f64,
627 pub v_max: f64,
629 pub pseudo_fraction: f64,
631 pub temperature: f64,
633}
634
635impl Supercapacitor {
636 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 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 pub fn capacitance(&self) -> f64 {
664 self.c_specific * self.electrode_area * (1.0 + self.pseudo_fraction)
665 }
666
667 pub fn energy_stored(&self, v: f64) -> f64 {
669 0.5 * self.capacitance() * v * v
670 }
671
672 pub fn max_energy(&self) -> f64 {
674 self.energy_stored(self.v_max)
675 }
676
677 pub fn peak_power(&self) -> f64 {
679 self.v_max * self.v_max / (4.0 * self.r_esr)
680 }
681
682 pub fn specific_energy_wh_kg(&self, m_kg: f64) -> f64 {
684 self.max_energy() / (m_kg * 3600.0)
685 }
686
687 pub fn time_constant(&self) -> f64 {
689 self.r_esr * self.capacitance()
690 }
691
692 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 pub fn charge_stored(&self, v: f64) -> f64 {
700 self.capacitance() * v
701 }
702
703 pub fn helmholtz_capacitance(epsilon_r: f64, d_debye: f64) -> f64 {
705 8.854187817e-12 * epsilon_r / d_debye
706 }
707
708 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; (eps * R * temperature / (2.0 * F_FARADAY * F_FARADAY * c_mol_m3 * z * z)).sqrt()
713 }
714}
715
716#[derive(Clone, Debug)]
722pub struct MetalHydride {
723 pub max_capacity_wt: f64,
725 pub ea_absorption: f64,
727 pub ea_desorption: f64,
729 pub k0_absorption: f64,
731 pub k0_desorption: f64,
733 pub p_eq_298: f64,
735 pub delta_h: f64,
737 pub delta_s: f64,
739 pub temperature: f64,
741}
742
743impl MetalHydride {
744 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 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 pub fn equilibrium_pressure(&self, t: f64) -> f64 {
776 let p0 = 1.0e5; p0 * (self.delta_h / (R * t) - self.delta_s / R).exp()
778 }
779
780 pub fn absorption_rate_constant(&self, t: f64) -> f64 {
782 self.k0_absorption * (-self.ea_absorption / (R * t)).exp()
783 }
784
785 pub fn desorption_rate_constant(&self, t: f64) -> f64 {
787 self.k0_desorption * (-self.ea_desorption / (R * t)).exp()
788 }
789
790 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 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 pub fn absorption_heat(&self, rate_wt_per_s: f64) -> f64 {
814 rate_wt_per_s * (-self.delta_h) / 2.016 }
817}
818
819#[derive(Clone, Debug)]
825pub struct NuclearMaterial {
826 pub name: String,
828 pub e_displacement: f64,
830 pub swelling_rate: f64,
832 pub hardening_coeff: f64,
834 pub creep_coefficient: f64,
836 pub dbtt_shift_per_dpa: f64,
838 pub yield_stress_0: f64,
840 pub modulus: f64,
842}
843
844impl NuclearMaterial {
845 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 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 pub fn dpa_from_fluence(&self, fluence: f64, sigma_d: f64) -> f64 {
877 fluence * sigma_d
879 }
880
881 pub fn swelling(&self, dpa: f64) -> f64 {
883 if dpa < 1.0 {
885 return 0.0;
886 }
887 self.swelling_rate * (dpa - 1.0) / 100.0
888 }
889
890 pub fn irradiation_hardening(&self, dpa: f64) -> f64 {
892 self.hardening_coeff * dpa.sqrt()
893 }
894
895 pub fn irradiated_yield_stress(&self, dpa: f64) -> f64 {
897 self.yield_stress_0 + self.irradiation_hardening(dpa)
898 }
899
900 pub fn irradiation_creep_rate(&self, sigma: f64, dpas_per_s: f64) -> f64 {
902 self.creep_coefficient * sigma * dpas_per_s
903 }
904
905 pub fn dbtt_shift(&self, dpa: f64) -> f64 {
907 self.dbtt_shift_per_dpa * dpa
908 }
909
910 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 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#[derive(Clone, Debug)]
933pub struct PiezoHarvester {
934 pub d31: f64,
936 pub k31: f64,
938 pub e_piezo: f64,
940 pub density: f64,
942 pub length: f64,
944 pub width: f64,
946 pub thickness: f64,
948 pub permittivity: f64,
950 pub damping: f64,
952 pub proof_mass: f64,
954}
955
956impl PiezoHarvester {
957 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 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 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 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 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 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 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 pub fn g31(&self) -> f64 {
1017 self.d31 / self.permittivity
1018 }
1019
1020 pub fn quality_factor(&self) -> f64 {
1022 1.0 / (2.0 * self.damping)
1023 }
1024
1025 pub fn coupling_efficiency(&self) -> f64 {
1027 let k2 = self.k31 * self.k31;
1028 k2 / (1.0 + k2)
1029 }
1030}
1031
1032#[derive(Clone, Debug)]
1038pub struct CapacityFadingModel {
1039 pub q0: f64,
1041 pub k_sei: f64,
1043 pub k_cal: f64,
1045 pub cycle_exp: f64,
1047}
1048
1049impl CapacityFadingModel {
1050 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 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 pub fn capacity_calendar(&self, t_hours: f64) -> f64 {
1067 self.q0 * (1.0 - self.k_sei * t_hours.sqrt())
1068 }
1069
1070 pub fn state_of_health(&self, q_current: f64) -> f64 {
1072 q_current / self.q0
1073 }
1074
1075 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#[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 #[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 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 #[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 #[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 #[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 #[test]
1215 fn test_zt_bi2te3() {
1216 let te = Thermoelectric::bi2te3();
1217 let zt = te.zt();
1218 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 #[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 assert!(ld > 1e-9 && ld < 1e-6);
1285 }
1286
1287 #[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 #[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 #[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 #[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 #[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}