1#[allow(unused_imports)]
6use super::functions::*;
7use super::functions::{
8 BOLTZMANN_K, SIGMA, blackbody_emissive_power, blackbody_spectral_intensity, wien_displacement,
9};
10
11#[derive(Debug, Clone, Copy)]
20#[allow(dead_code)]
21pub struct SolarCell {
22 pub i_ph: f64,
24 pub i_0: f64,
26 pub n_ideal: f64,
28 pub temperature: f64,
30 pub r_series: f64,
32}
33impl SolarCell {
34 #[allow(dead_code)]
36 pub fn new(i_ph: f64, i_0: f64, n_ideal: f64, temperature: f64, r_series: f64) -> Self {
37 Self {
38 i_ph,
39 i_0,
40 n_ideal,
41 temperature,
42 r_series,
43 }
44 }
45 #[allow(dead_code)]
47 pub fn thermal_voltage(&self) -> f64 {
48 BOLTZMANN_K * self.temperature / 1.602e-19
49 }
50 #[allow(dead_code)]
52 pub fn short_circuit_current(&self) -> f64 {
53 self.i_ph - self.i_0 * (1.0 / (self.n_ideal * self.thermal_voltage())).exp()
54 }
55 #[allow(dead_code)]
59 pub fn current_at_voltage(&self, v: f64) -> f64 {
60 let v_t = self.thermal_voltage();
61 (self.i_ph - self.i_0 * ((v / (self.n_ideal * v_t)).exp() - 1.0)).max(0.0)
62 }
63 #[allow(dead_code)]
65 pub fn power_at_voltage(&self, v: f64) -> f64 {
66 v * self.current_at_voltage(v)
67 }
68 #[allow(dead_code)]
73 pub fn open_circuit_voltage(&self) -> f64 {
74 if self.i_0 < f64::EPSILON {
75 return 0.0;
76 }
77 self.n_ideal * self.thermal_voltage() * (self.i_ph / self.i_0 + 1.0).ln()
78 }
79 #[allow(dead_code)]
85 pub fn fill_factor(&self) -> f64 {
86 let v_t = self.thermal_voltage();
87 let v_oc = self.open_circuit_voltage();
88 let v_oc_norm = v_oc / (self.n_ideal * v_t);
89 if v_oc_norm < 1.0 {
90 return 0.0;
91 }
92 (v_oc_norm - (v_oc_norm + 0.72).ln()) / (v_oc_norm + 1.0)
93 }
94 #[allow(dead_code)]
100 pub fn efficiency(&self, irradiance: f64, area: f64) -> f64 {
101 let p_in = irradiance * area;
102 if p_in < f64::EPSILON {
103 return 0.0;
104 }
105 let i_sc = self.short_circuit_current();
106 let v_oc = self.open_circuit_voltage();
107 let ff = self.fill_factor();
108 ff * i_sc * v_oc / p_in
109 }
110}
111#[derive(Debug, Clone)]
113#[allow(dead_code)]
114pub struct RadiationSurface {
115 pub area: f64,
117 pub emissivity: f64,
119 pub temperature: f64,
121 pub name: String,
123}
124impl RadiationSurface {
125 #[allow(dead_code)]
127 pub fn new(area: f64, emissivity: f64, temp_k: f64, name: &str) -> Self {
128 Self {
129 area,
130 emissivity,
131 temperature: temp_k,
132 name: name.to_string(),
133 }
134 }
135 #[allow(dead_code)]
137 pub fn radiosity(&self) -> f64 {
138 self.emissivity * SIGMA * self.temperature.powi(4)
139 }
140 #[allow(dead_code)]
144 pub fn surface_resistance(&self) -> f64 {
145 if (self.emissivity - 1.0).abs() < 1e-12 {
146 0.0
147 } else {
148 (1.0 - self.emissivity) / (self.emissivity * self.area)
149 }
150 }
151}
152#[derive(Debug, Clone, Copy)]
154#[allow(dead_code)]
155pub struct NeutronModeration {
156 pub sigma_s: f64,
158 pub sigma_a: f64,
160 pub xi: f64,
162}
163impl NeutronModeration {
164 #[allow(dead_code)]
166 pub fn new(sigma_s: f64, sigma_a: f64, xi: f64) -> Self {
167 Self {
168 sigma_s,
169 sigma_a,
170 xi,
171 }
172 }
173 #[allow(dead_code)]
175 pub fn slowing_down_power(&self) -> f64 {
176 self.xi * self.sigma_s
177 }
178 #[allow(dead_code)]
180 pub fn moderation_ratio(&self) -> f64 {
181 if self.sigma_a < f64::EPSILON {
182 return f64::INFINITY;
183 }
184 self.xi * self.sigma_s / self.sigma_a
185 }
186 #[allow(dead_code)]
188 pub fn migration_length_sq(&self) -> f64 {
189 let d = 1.0 / (3.0 * self.sigma_s);
190 d / self.sigma_a
191 }
192}
193#[derive(Debug, Clone, Copy)]
198#[allow(dead_code)]
199pub struct BlackbodySpectrum {
200 pub temp_k: f64,
202}
203impl BlackbodySpectrum {
204 #[allow(dead_code)]
206 pub fn new(temp_k: f64) -> Self {
207 Self { temp_k }
208 }
209 #[allow(dead_code)]
211 pub fn planck(&self, lambda_m: f64) -> f64 {
212 blackbody_spectral_intensity(lambda_m, self.temp_k)
213 }
214 #[allow(dead_code)]
216 pub fn total_power(&self) -> f64 {
217 blackbody_emissive_power(self.temp_k)
218 }
219 #[allow(dead_code)]
221 pub fn peak_wavelength(&self) -> f64 {
222 wien_displacement(self.temp_k)
223 }
224 #[allow(dead_code)]
227 pub fn band_fraction(&self, lambda_a: f64, lambda_b: f64, n_steps: usize) -> f64 {
228 let eb = self.total_power();
229 if eb < f64::EPSILON {
230 return 0.0;
231 }
232 let n = n_steps.max(2);
233 let dlam = (lambda_b - lambda_a) / (n as f64);
234 let mut sum = 0.0;
235 for i in 0..=n {
236 let lam = lambda_a + (i as f64) * dlam;
237 let w = if i == 0 || i == n { 0.5 } else { 1.0 };
238 sum += w * std::f64::consts::PI * self.planck(lam);
239 }
240 (sum * dlam / eb).clamp(0.0, 1.0)
241 }
242}
243#[allow(dead_code)]
251#[derive(Debug, Clone)]
252pub struct MonteCarloViewFactor {
253 pub n_rays: usize,
255 pub(super) seed: u64,
257}
258impl MonteCarloViewFactor {
259 pub fn new(n_rays: usize) -> Self {
261 Self {
262 n_rays,
263 seed: 12345678901,
264 }
265 }
266 fn rand_next(&mut self) -> f64 {
268 self.seed = self
269 .seed
270 .wrapping_mul(6_364_136_223_846_793_005)
271 .wrapping_add(1_442_695_040_888_963_407);
272 (self.seed >> 33) as f64 / (u32::MAX as f64)
273 }
274 pub fn parallel_disks(&mut self, r1: f64, r2: f64, h: f64) -> f64 {
284 let mut hits = 0usize;
285 for _ in 0..self.n_rays {
286 let r_orig = r1 * self.rand_next().sqrt();
287 let phi_orig = 2.0 * std::f64::consts::PI * self.rand_next();
288 let ox = r_orig * phi_orig.cos();
289 let oy = r_orig * phi_orig.sin();
290 let theta = (self.rand_next()).sqrt().asin();
291 let phi_dir = 2.0 * std::f64::consts::PI * self.rand_next();
292 let dz = theta.cos();
293 let dx = theta.sin() * phi_dir.cos();
294 let dy = theta.sin() * phi_dir.sin();
295 if dz < 1e-12 {
296 continue;
297 }
298 let t = h / dz;
299 let ix = ox + dx * t;
300 let iy = oy + dy * t;
301 if ix * ix + iy * iy <= r2 * r2 {
302 hits += 1;
303 }
304 }
305 hits as f64 / self.n_rays as f64
306 }
307}
308#[allow(dead_code)]
315#[derive(Debug, Clone)]
316pub struct RadiationNetwork {
317 pub n: usize,
319 pub temperatures: Vec<f64>,
321 pub emissivities: Vec<f64>,
323 pub areas: Vec<f64>,
325 pub view_factors: Vec<f64>,
327}
328impl RadiationNetwork {
329 pub fn new(
331 temperatures: Vec<f64>,
332 emissivities: Vec<f64>,
333 areas: Vec<f64>,
334 view_factors: Vec<f64>,
335 ) -> Self {
336 let n = temperatures.len();
337 assert_eq!(emissivities.len(), n);
338 assert_eq!(areas.len(), n);
339 assert_eq!(view_factors.len(), n * n);
340 Self {
341 n,
342 temperatures,
343 emissivities,
344 areas,
345 view_factors,
346 }
347 }
348 pub fn f(&self, i: usize, j: usize) -> f64 {
350 self.view_factors[i * self.n + j]
351 }
352 pub fn net_heat_flow_simple(&self, i: usize) -> f64 {
358 let emit_i = self.emissivities[i] * SIGMA * self.temperatures[i].powi(4) * self.areas[i];
359 let absorbed: f64 = (0..self.n)
360 .map(|j| {
361 self.f(i, j)
362 * self.emissivities[j]
363 * SIGMA
364 * self.temperatures[j].powi(4)
365 * self.areas[i]
366 })
367 .sum();
368 emit_i - absorbed
369 }
370 pub fn emitted_power(&self, i: usize) -> f64 {
372 self.emissivities[i] * SIGMA * self.temperatures[i].powi(4) * self.areas[i]
373 }
374}
375#[derive(Debug, Clone)]
380#[allow(dead_code)]
381pub struct SpectralEmissivityModel {
382 pub wavelengths: Vec<f64>,
384 pub emissivities: Vec<f64>,
386}
387impl SpectralEmissivityModel {
388 #[allow(dead_code)]
392 pub fn new(wavelengths: Vec<f64>, emissivities: Vec<f64>) -> Self {
393 assert_eq!(
394 wavelengths.len(),
395 emissivities.len(),
396 "wavelengths and emissivities must have the same length"
397 );
398 Self {
399 wavelengths,
400 emissivities,
401 }
402 }
403 #[allow(dead_code)]
407 pub fn emissivity_at(&self, lambda: f64) -> f64 {
408 let n = self.wavelengths.len();
409 if n == 0 {
410 return 0.0;
411 }
412 if n == 1 {
413 return self.emissivities[0];
414 }
415 if lambda <= self.wavelengths[0] {
416 return self.emissivities[0];
417 }
418 if lambda >= self.wavelengths[n - 1] {
419 return self.emissivities[n - 1];
420 }
421 for i in 0..n - 1 {
422 let w0 = self.wavelengths[i];
423 let w1 = self.wavelengths[i + 1];
424 if lambda >= w0 && lambda <= w1 {
425 let t = (lambda - w0) / (w1 - w0);
426 return self.emissivities[i] * (1.0 - t) + self.emissivities[i + 1] * t;
427 }
428 }
429 self.emissivities[n - 1]
430 }
431 #[allow(dead_code)]
437 pub fn effective_total_emissivity(&self, temp_k: f64, n_steps: usize) -> f64 {
438 let lambda_a = 100e-9_f64;
439 let lambda_b = 100e-6_f64;
440 let n = n_steps.max(2);
441 let dl = (lambda_b - lambda_a) / n as f64;
442 let e_b_total = blackbody_emissive_power(temp_k);
443 if e_b_total < f64::EPSILON {
444 return 0.0;
445 }
446 let mut sum = 0.0;
447 for i in 0..=n {
448 let lam = lambda_a + i as f64 * dl;
449 let w = if i == 0 || i == n { 0.5 } else { 1.0 };
450 let eps = self.emissivity_at(lam);
451 let e_b_lam = std::f64::consts::PI * blackbody_spectral_intensity(lam, temp_k);
452 sum += w * eps * e_b_lam;
453 }
454 (sum * dl / e_b_total).clamp(0.0, 1.0)
455 }
456}
457#[derive(Debug, Clone, Copy)]
462#[allow(dead_code)]
463pub struct IrradiationDosimetry {
464 pub flux: f64,
466 pub time_s: f64,
468 pub kerma_cross_section: f64,
470}
471impl IrradiationDosimetry {
472 #[allow(dead_code)]
474 pub fn new(flux: f64, time_s: f64, kerma_cross_section: f64) -> Self {
475 Self {
476 flux,
477 time_s,
478 kerma_cross_section,
479 }
480 }
481 #[allow(dead_code)]
483 pub fn fluence(&self) -> f64 {
484 self.flux * self.time_s
485 }
486 #[allow(dead_code)]
492 pub fn absorbed_dose_gray(&self) -> f64 {
493 let e_transfer = 1.602e-13;
494 self.fluence() * self.kerma_cross_section * 1.0e4 * e_transfer
495 }
496 #[allow(dead_code)]
500 pub fn kerma_rate(&self, rho: f64) -> f64 {
501 let e_transfer = 1.602e-13;
502 self.flux * self.kerma_cross_section * 1.0e4 * e_transfer / rho
503 }
504 #[allow(dead_code)]
508 pub fn dose_equivalent_sievert(&self, quality_factor: f64) -> f64 {
509 self.absorbed_dose_gray() * quality_factor
510 }
511 #[allow(dead_code)]
513 pub fn rem_dose(&self, quality_factor: f64) -> f64 {
514 self.dose_equivalent_sievert(quality_factor) / 0.01
515 }
516}
517#[derive(Debug, Clone, Copy)]
522#[allow(dead_code)]
523pub struct StefanBoltzmannIntegrator {
524 pub temp_k: f64,
526}
527impl StefanBoltzmannIntegrator {
528 #[allow(dead_code)]
530 pub fn new(temp_k: f64) -> Self {
531 Self { temp_k }
532 }
533 #[allow(dead_code)]
537 pub fn integrate_full_spectrum(&self, n_steps: usize) -> f64 {
538 let lambda_a = 100e-9_f64;
539 let lambda_b = 1e-3_f64;
540 self.integrate_range(lambda_a, lambda_b, n_steps)
541 }
542 #[allow(dead_code)]
546 pub fn integrate_range(&self, lambda_a: f64, lambda_b: f64, n_steps: usize) -> f64 {
547 let n = if n_steps.is_multiple_of(2) {
548 n_steps
549 } else {
550 n_steps + 1
551 };
552 let n = n.max(2);
553 let dl = (lambda_b - lambda_a) / n as f64;
554 let mut sum = 0.0;
555 for i in 0..=n {
556 let lam = lambda_a + i as f64 * dl;
557 let intensity = std::f64::consts::PI * blackbody_spectral_intensity(lam, self.temp_k);
558 let w = if i == 0 || i == n {
559 1.0
560 } else if i % 2 == 1 {
561 4.0
562 } else {
563 2.0
564 };
565 sum += w * intensity;
566 }
567 sum * dl / 3.0
568 }
569 #[allow(dead_code)]
573 pub fn band_fraction_in_range(&self, lambda_a: f64, lambda_b: f64, n_steps: usize) -> f64 {
574 let total = blackbody_emissive_power(self.temp_k);
575 if total < f64::EPSILON {
576 return 0.0;
577 }
578 let band = self.integrate_range(lambda_a, lambda_b, n_steps);
579 (band / total).clamp(0.0, 1.0)
580 }
581 #[allow(dead_code)]
587 pub fn weighted_emissivity(&self, emissivity_fn: impl Fn(f64) -> f64, n_steps: usize) -> f64 {
588 let total = blackbody_emissive_power(self.temp_k);
589 if total < f64::EPSILON {
590 return 0.0;
591 }
592 let lambda_a = 100e-9_f64;
593 let lambda_b = 1e-3_f64;
594 let n = n_steps.max(2);
595 let dl = (lambda_b - lambda_a) / n as f64;
596 let mut sum = 0.0;
597 for i in 0..=n {
598 let lam = lambda_a + i as f64 * dl;
599 let w = if i == 0 || i == n { 0.5 } else { 1.0 };
600 let eps = emissivity_fn(lam);
601 let e_b_lam = std::f64::consts::PI * blackbody_spectral_intensity(lam, self.temp_k);
602 sum += w * eps * e_b_lam;
603 }
604 (sum * dl / total).clamp(0.0, 1.0)
605 }
606}
607#[derive(Debug, Clone, Copy)]
610#[allow(dead_code)]
611pub struct RadiativeProperties {
612 pub emissivity: f64,
614 pub absorptivity: f64,
616 pub transmissivity: f64,
618 pub reflectivity: f64,
620}
621impl RadiativeProperties {
622 #[allow(dead_code)]
629 pub fn new(emissivity: f64, transmissivity: f64) -> Self {
630 let absorptivity = emissivity;
631 let reflectivity = (1.0 - absorptivity - transmissivity).max(0.0);
632 Self {
633 emissivity,
634 absorptivity,
635 transmissivity,
636 reflectivity,
637 }
638 }
639 #[allow(dead_code)]
641 pub fn is_consistent(&self) -> bool {
642 let sum = self.absorptivity + self.transmissivity + self.reflectivity;
643 (sum - 1.0).abs() < 1.0e-9
644 }
645 #[allow(dead_code)]
647 pub fn emissive_power(&self, temp_k: f64) -> f64 {
648 self.emissivity * SIGMA * temp_k.powi(4)
649 }
650}
651#[allow(dead_code)]
658#[derive(Debug, Clone)]
659pub struct ParticipatingMedia {
660 pub kappa: f64,
662 pub sigma_s: f64,
664 pub temperature: f64,
666}
667impl ParticipatingMedia {
668 pub fn new(kappa: f64, sigma_s: f64, temperature: f64) -> Self {
670 Self {
671 kappa,
672 sigma_s,
673 temperature,
674 }
675 }
676 pub fn extinction(&self) -> f64 {
678 self.kappa + self.sigma_s
679 }
680 pub fn albedo(&self) -> f64 {
682 let b = self.extinction();
683 if b < f64::EPSILON {
684 return 0.0;
685 }
686 self.sigma_s / b
687 }
688 pub fn optical_depth(&self, path_length: f64) -> f64 {
690 self.extinction() * path_length
691 }
692 pub fn transmittance(&self, path_length: f64) -> f64 {
694 (-self.optical_depth(path_length)).exp()
695 }
696 pub fn emission_optically_thin(&self, path_length: f64) -> f64 {
700 self.kappa * SIGMA * self.temperature.powi(4) * path_length
701 }
702 pub fn effective_emissivity(&self, path_length: f64) -> f64 {
706 1.0 - (-self.kappa * path_length).exp()
707 }
708 pub fn mean_beam_length_sphere(radius: f64) -> f64 {
710 0.65 * 2.0 * radius
711 }
712 pub fn mean_beam_length_cylinder(radius: f64) -> f64 {
714 0.95 * 2.0 * radius
715 }
716}
717#[derive(Debug, Clone, Copy)]
721#[allow(dead_code)]
722pub struct RadiationDamage {
723 pub fluence: f64,
725 pub displacement_cross_section: f64,
727 pub atomic_density: f64,
729}
730impl RadiationDamage {
731 #[allow(dead_code)]
733 pub fn new(fluence: f64, sigma_d: f64, n_atoms: f64) -> Self {
734 Self {
735 fluence,
736 displacement_cross_section: sigma_d,
737 atomic_density: n_atoms,
738 }
739 }
740 #[allow(dead_code)]
745 pub fn dpa(&self) -> f64 {
746 self.fluence * self.displacement_cross_section
747 }
748 #[allow(dead_code)]
750 pub fn displaced_fraction(&self) -> f64 {
751 self.dpa().min(1.0)
752 }
753 #[allow(dead_code)]
757 pub fn swelling_fraction(&self) -> f64 {
758 let dpa = self.dpa();
759 1.0e-3 * dpa * dpa
760 }
761}
762#[derive(Debug, Clone, Copy)]
767#[allow(dead_code)]
768pub struct GrayBodyModel {
769 pub eps_ref: f64,
771 pub deps_dt: f64,
773 pub temp_ref: f64,
775}
776impl GrayBodyModel {
777 #[allow(dead_code)]
779 pub fn new(eps_ref: f64, deps_dt: f64, temp_ref: f64) -> Self {
780 Self {
781 eps_ref,
782 deps_dt,
783 temp_ref,
784 }
785 }
786 #[allow(dead_code)]
788 pub fn emissivity(&self, temp_k: f64) -> f64 {
789 (self.eps_ref + self.deps_dt * (temp_k - self.temp_ref)).clamp(0.0, 1.0)
790 }
791 #[allow(dead_code)]
793 pub fn emissive_power(&self, temp_k: f64) -> f64 {
794 self.emissivity(temp_k) * SIGMA * temp_k.powi(4)
795 }
796 #[allow(dead_code)]
801 pub fn effective_blackbody_temperature(&self, temp_k: f64) -> f64 {
802 let eps = self.emissivity(temp_k);
803 temp_k * eps.powf(0.25)
804 }
805}
806#[derive(Debug, Clone)]
811#[allow(dead_code)]
812pub struct MCRadiationTransport {
813 pub n_photons: usize,
815 pub(super) seed: u64,
817}
818impl MCRadiationTransport {
819 #[allow(dead_code)]
821 pub fn new(n_photons: usize, seed: u64) -> Self {
822 Self { n_photons, seed }
823 }
824 fn rand_next(&mut self) -> f64 {
826 self.seed = self
827 .seed
828 .wrapping_mul(6_364_136_223_846_793_005)
829 .wrapping_add(1_442_695_040_888_963_407);
830 (self.seed >> 33) as f64 / (u32::MAX as f64)
831 }
832 #[allow(dead_code)]
838 pub fn slab_transmittance(&mut self, kappa: f64, thickness: f64) -> f64 {
839 self.slab_transmittance_full(kappa, thickness, 0.0)
840 }
841 #[allow(dead_code)]
848 pub fn slab_transmittance_full(&mut self, kappa: f64, thickness: f64, albedo: f64) -> f64 {
849 if self.n_photons == 0 {
850 return 0.0;
851 }
852 let beta = kappa / (1.0 - albedo).max(1e-15);
853 let mut transmitted = 0usize;
854 for _ in 0..self.n_photons {
855 let mut x = 0.0_f64;
856 let mut weight = 1.0_f64;
857 let mut alive = true;
858 while alive && x < thickness {
859 let xi = self.rand_next().max(1e-15);
860 let s = -xi.ln() / beta;
861 x += s;
862 if x >= thickness {
863 transmitted += 1;
864 break;
865 }
866 if albedo < f64::EPSILON {
867 alive = false;
868 } else {
869 weight *= albedo;
870 if weight < 0.001 {
871 let rr = self.rand_next();
872 if rr < 0.1 {
873 weight /= 0.1;
874 } else {
875 alive = false;
876 }
877 }
878 let cos_theta = 2.0 * self.rand_next() - 1.0;
879 if cos_theta < 0.0 {
880 alive = false;
881 }
882 }
883 }
884 }
885 transmitted as f64 / self.n_photons as f64
886 }
887 #[allow(dead_code)]
891 pub fn estimate_mean_free_path(&mut self, beta: f64) -> f64 {
892 let mut total_path = 0.0;
893 let n = self.n_photons.max(1);
894 for _ in 0..n {
895 let xi = self.rand_next().max(1e-15);
896 total_path += -xi.ln() / beta;
897 }
898 total_path / n as f64
899 }
900}