Skip to main content

oxiphysics_materials/radiation/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#[allow(unused_imports)]
6use super::functions::*;
7use super::functions::{
8    BOLTZMANN_K, SIGMA, blackbody_emissive_power, blackbody_spectral_intensity, wien_displacement,
9};
10
11/// Simple single-diode solar cell model.
12///
13/// Computes the current–voltage (I–V) characteristic of an ideal single-diode
14/// solar cell using the Shockley equation:
15///
16/// `I = I_ph - I_0 * (exp(q*V/(n*k*T)) - 1)`
17///
18/// where `I_ph` is the photocurrent and `I_0` is the dark saturation current.
19#[derive(Debug, Clone, Copy)]
20#[allow(dead_code)]
21pub struct SolarCell {
22    /// Photocurrent \[A\]
23    pub i_ph: f64,
24    /// Dark saturation current \[A\]
25    pub i_0: f64,
26    /// Ideality factor (1 for ideal)
27    pub n_ideal: f64,
28    /// Temperature \[K\]
29    pub temperature: f64,
30    /// Series resistance \[Ω\]
31    pub r_series: f64,
32}
33impl SolarCell {
34    /// Create a new solar cell model.
35    #[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    /// Thermal voltage \[V\]: `V_t = k*T/q`.
46    #[allow(dead_code)]
47    pub fn thermal_voltage(&self) -> f64 {
48        BOLTZMANN_K * self.temperature / 1.602e-19
49    }
50    /// Short-circuit current \[A\] (at V = 0): `I_sc ≈ I_ph`.
51    #[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    /// Current \[A\] at terminal voltage `v` \[V\].
56    ///
57    /// Neglects series resistance for simplicity (direct explicit form).
58    #[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    /// Power \[W\] at terminal voltage `v` \[V\].
64    #[allow(dead_code)]
65    pub fn power_at_voltage(&self, v: f64) -> f64 {
66        v * self.current_at_voltage(v)
67    }
68    /// Open-circuit voltage \[V\]: solved numerically (bisection).
69    ///
70    /// At V_oc: I = 0 → `I_ph = I_0*(exp(V_oc/(n*V_t)) - 1)`.
71    /// Exact: `V_oc = n*V_t * ln(I_ph/I_0 + 1)`.
72    #[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    /// Fill factor (FF): `FF = P_max / (I_sc * V_oc)`.
80    ///
81    /// Estimated using the empirical Green formula:
82    /// `FF ≈ (v_oc - ln(v_oc + 0.72)) / (v_oc + 1)`
83    /// where `v_oc = V_oc / V_t` (normalised open-circuit voltage).
84    #[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    /// Efficiency: `η = P_max / P_in` where `P_max = FF * I_sc * V_oc`.
95    ///
96    /// # Arguments
97    /// * `irradiance` — incident irradiance \[W/m²\]
98    /// * `area`       — cell area \[m²\]
99    #[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/// A gray, diffuse radiation surface for network analysis.
112#[derive(Debug, Clone)]
113#[allow(dead_code)]
114pub struct RadiationSurface {
115    /// Surface area \[m²\]
116    pub area: f64,
117    /// Surface emissivity (0–1)
118    pub emissivity: f64,
119    /// Surface temperature \[K\]
120    pub temperature: f64,
121    /// Descriptive name
122    pub name: String,
123}
124impl RadiationSurface {
125    /// Create a new radiation surface.
126    #[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    /// Radiosity \[W/m²\]: simplified as J = epsilon * sigma * T^4.
136    #[allow(dead_code)]
137    pub fn radiosity(&self) -> f64 {
138        self.emissivity * SIGMA * self.temperature.powi(4)
139    }
140    /// Surface (blackbody) resistance \[(W/m²)^{-1}\]: (1 - epsilon) / (epsilon * A).
141    ///
142    /// Returns infinity for a blackbody (epsilon = 1).
143    #[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/// Neutron moderation properties for a moderator material.
153#[derive(Debug, Clone, Copy)]
154#[allow(dead_code)]
155pub struct NeutronModeration {
156    /// Macroscopic scattering cross-section Σ_s \[cm⁻¹\]
157    pub sigma_s: f64,
158    /// Macroscopic absorption cross-section Σ_a \[cm⁻¹\]
159    pub sigma_a: f64,
160    /// Average logarithmic energy decrement ξ (dimensionless)
161    pub xi: f64,
162}
163impl NeutronModeration {
164    /// Create a new `NeutronModeration` model.
165    #[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    /// Slowing-down power \[cm⁻¹\]: `SDP = ξ · Σ_s`.
174    #[allow(dead_code)]
175    pub fn slowing_down_power(&self) -> f64 {
176        self.xi * self.sigma_s
177    }
178    /// Moderation ratio (figure of merit): `MR = ξ · Σ_s / Σ_a`.
179    #[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    /// Migration length squared \[cm²\]: `M² = D / Σ_a` where `D = 1/(3·Σ_s)`.
187    #[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/// Blackbody spectrum utilities.
194///
195/// Groups the Planck function, Stefan-Boltzmann total power, and Wien peak
196/// for a surface at temperature `temp_k`.
197#[derive(Debug, Clone, Copy)]
198#[allow(dead_code)]
199pub struct BlackbodySpectrum {
200    /// Surface temperature \[K\]
201    pub temp_k: f64,
202}
203impl BlackbodySpectrum {
204    /// Create a new `BlackbodySpectrum` at temperature `temp_k`.
205    #[allow(dead_code)]
206    pub fn new(temp_k: f64) -> Self {
207        Self { temp_k }
208    }
209    /// Spectral radiance `B(λ, T)` \[W/(m²·sr·m)\] at wavelength `lambda_m` \[m\].
210    #[allow(dead_code)]
211    pub fn planck(&self, lambda_m: f64) -> f64 {
212        blackbody_spectral_intensity(lambda_m, self.temp_k)
213    }
214    /// Total emissive power `E_b = σ·T⁴` \[W/m²\].
215    #[allow(dead_code)]
216    pub fn total_power(&self) -> f64 {
217        blackbody_emissive_power(self.temp_k)
218    }
219    /// Peak wavelength via Wien's displacement law \[m\].
220    #[allow(dead_code)]
221    pub fn peak_wavelength(&self) -> f64 {
222        wien_displacement(self.temp_k)
223    }
224    /// Fractional emissive power in wavelength range \[lambda_a, lambda_b\] via
225    /// a simple trapezoidal quadrature over `n_steps` intervals.
226    #[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/// Simple Monte Carlo ray tracer for view factor estimation.
244///
245/// Estimates the view factor F12 between two planar surfaces by firing
246/// random rays from surface 1 and counting how many hit surface 2.
247///
248/// For reproducible tests a seeded "pseudo-random" deterministic sequence
249/// is used (simple LCG) so no external rand dependency is needed here.
250#[allow(dead_code)]
251#[derive(Debug, Clone)]
252pub struct MonteCarloViewFactor {
253    /// Number of rays per calculation.
254    pub n_rays: usize,
255    /// LCG seed for reproducibility.
256    pub(super) seed: u64,
257}
258impl MonteCarloViewFactor {
259    /// Create a new Monte Carlo view factor calculator.
260    pub fn new(n_rays: usize) -> Self {
261        Self {
262            n_rays,
263            seed: 12345678901,
264        }
265    }
266    /// Simple 64-bit LCG random number in \[0, 1).
267    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    /// Estimate the view factor for two coaxial parallel disks using MC.
275    ///
276    /// Uses the hemisphere cosine sampling method.  Returns an estimate
277    /// of F12 (fraction of rays from disk 1 that hit disk 2).
278    ///
279    /// # Arguments
280    /// * `r1` — radius of disk 1
281    /// * `r2` — radius of disk 2
282    /// * `h`  — separation distance
283    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/// N-surface radiation network (gray, diffuse enclosure).
309///
310/// Solves the full N×N radiosity system for a closed diffuse gray enclosure.
311/// The net heat flow on each surface is computed from the radiosity vector.
312///
313/// Reference: Incropera et al., "Fundamentals of Heat and Mass Transfer".
314#[allow(dead_code)]
315#[derive(Debug, Clone)]
316pub struct RadiationNetwork {
317    /// Number of surfaces.
318    pub n: usize,
319    /// Surface temperatures \[K\].
320    pub temperatures: Vec<f64>,
321    /// Surface emissivities (0–1).
322    pub emissivities: Vec<f64>,
323    /// Surface areas \[m²\].
324    pub areas: Vec<f64>,
325    /// View factor matrix F\[i\]\[j\] (row-major, n×n).
326    pub view_factors: Vec<f64>,
327}
328impl RadiationNetwork {
329    /// Create a new radiation network.
330    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    /// View factor F\[i\]\[j\].
349    pub fn f(&self, i: usize, j: usize) -> f64 {
350        self.view_factors[i * self.n + j]
351    }
352    /// Net heat flow from surface i \[W\] using the radiosity method.
353    ///
354    /// Simplified: q_i = ε_i·A_i·σ·(T_i⁴) - A_i·sum_j(F_ij · ε_j·σ·T_j⁴)
355    ///
356    /// This is the optically-simplified (no inter-reflection) version.
357    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    /// Total radiative power emitted by surface i \[W\].
371    pub fn emitted_power(&self, i: usize) -> f64 {
372        self.emissivities[i] * SIGMA * self.temperatures[i].powi(4) * self.areas[i]
373    }
374}
375/// Spectrally resolved emissivity model using piecewise linear interpolation.
376///
377/// Allows emissivity to vary with wavelength, which is important for
378/// selective emitters/absorbers in solar thermal and thermophotovoltaic systems.
379#[derive(Debug, Clone)]
380#[allow(dead_code)]
381pub struct SpectralEmissivityModel {
382    /// Wavelengths \[m\] at which emissivity is specified (sorted ascending).
383    pub wavelengths: Vec<f64>,
384    /// Emissivity values (0–1) at each wavelength knot.
385    pub emissivities: Vec<f64>,
386}
387impl SpectralEmissivityModel {
388    /// Create from matched wavelength and emissivity vectors.
389    ///
390    /// Wavelengths must be sorted in ascending order.
391    #[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    /// Linearly interpolate emissivity at wavelength `lambda` \[m\].
404    ///
405    /// Clamps to the first/last value outside the defined range.
406    #[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    /// Compute the total (hemispherical) emissivity at temperature `temp_k`
432    /// by integrating `ε(λ) * E_b(λ, T)` over all wavelengths.
433    ///
434    /// Uses simple trapezoidal rule with `n_steps` intervals over the range
435    /// \[100 nm, 100 µm\].
436    #[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/// Irradiation dosimetry model for neutron/gamma radiation exposure.
458///
459/// Computes absorbed dose, fluence, kerma, and dose-equivalent quantities
460/// for radiation shielding and materials damage analysis.
461#[derive(Debug, Clone, Copy)]
462#[allow(dead_code)]
463pub struct IrradiationDosimetry {
464    /// Particle flux \[particles/(cm²·s)\].
465    pub flux: f64,
466    /// Exposure time \[s\].
467    pub time_s: f64,
468    /// Energy-transfer cross-section σ_kin \[cm²\] (for kerma calculation).
469    pub kerma_cross_section: f64,
470}
471impl IrradiationDosimetry {
472    /// Create a new dosimetry model.
473    #[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    /// Total fluence \[particles/cm²\].
482    #[allow(dead_code)]
483    pub fn fluence(&self) -> f64 {
484        self.flux * self.time_s
485    }
486    /// Absorbed dose \[Gy = J/kg\] for a target material of density `rho_kg_m3`.
487    ///
488    /// D \[Gy\] = Φ · σ_kin · E_avg / ρ
489    ///
490    /// Uses a fixed average energy transfer of 1 MeV = 1.602e-13 J.
491    #[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    /// Kerma rate \[Gy/s\] for a material of density `rho` \[kg/m³\].
497    ///
498    /// `K = Φ · σ_kin · E_n / ρ_target`
499    #[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    /// Dose equivalent \[Sv\] = absorbed dose \[Gy\] × quality factor `qf`.
505    ///
506    /// Quality factors: photons/electrons = 1, neutrons = 5–20, alpha = 20.
507    #[allow(dead_code)]
508    pub fn dose_equivalent_sievert(&self, quality_factor: f64) -> f64 {
509        self.absorbed_dose_gray() * quality_factor
510    }
511    /// Equivalent rem dose (1 rem = 0.01 Sv).
512    #[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/// Numerical integrator for Stefan-Boltzmann-related spectral calculations.
518///
519/// Provides accurate band-limited and spectrally weighted integration of
520/// the Planck blackbody function using Gaussian quadrature (Simpson's rule).
521#[derive(Debug, Clone, Copy)]
522#[allow(dead_code)]
523pub struct StefanBoltzmannIntegrator {
524    /// Surface temperature \[K\].
525    pub temp_k: f64,
526}
527impl StefanBoltzmannIntegrator {
528    /// Create a new integrator for a blackbody at `temp_k`.
529    #[allow(dead_code)]
530    pub fn new(temp_k: f64) -> Self {
531        Self { temp_k }
532    }
533    /// Total integrated spectral irradiance over \[100 nm, 1 mm\] \[W/m²\].
534    ///
535    /// For a blackbody this should approach `σ·T⁴` when the range is wide enough.
536    #[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    /// Integrate spectral irradiance over \[lambda_a, lambda_b\] \[W/m²\].
543    ///
544    /// Uses Simpson's rule for accuracy.
545    #[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    /// Fraction of total blackbody power emitted in wavelength band \[lambda_a, lambda_b\].
570    ///
571    /// Returns a value in \[0, 1\].
572    #[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    /// Effective emissivity of a gray coating integrated over the Planck spectrum.
582    ///
583    /// `ε_eff = ∫ ε(λ) E_b(λ,T) dλ / E_b(T)`
584    ///
585    /// For a uniform emissivity this equals the emissivity itself.
586    #[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/// Radiative surface properties satisfying the energy balance
608/// `emissivity + reflectivity + transmissivity ≤ 1`.
609#[derive(Debug, Clone, Copy)]
610#[allow(dead_code)]
611pub struct RadiativeProperties {
612    /// Emissivity ε (0–1)
613    pub emissivity: f64,
614    /// Absorptivity α (0–1)
615    pub absorptivity: f64,
616    /// Transmissivity τ (0–1)
617    pub transmissivity: f64,
618    /// Reflectivity ρ = 1 − α − τ (derived, ≥ 0)
619    pub reflectivity: f64,
620}
621impl RadiativeProperties {
622    /// Create from emissivity `eps`, transmissivity `tau`, with
623    /// absorptivity set via Kirchhoff's law (`absorptivity = emissivity`)
624    /// and reflectivity derived from the energy balance.
625    ///
626    /// # Panics
627    /// Panics in debug mode if `eps + tau > 1`.
628    #[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    /// Returns `true` if the energy balance is satisfied within tolerance.
640    #[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    /// Effective emissive power \[W/m²\] for a surface at temperature `temp_k`.
646    #[allow(dead_code)]
647    pub fn emissive_power(&self, temp_k: f64) -> f64 {
648        self.emissivity * SIGMA * temp_k.powi(4)
649    }
650}
651/// Participating media (gas radiation) model.
652///
653/// Models radiative transfer through an absorbing/emitting gas via
654/// the mean-beam-length approximation.
655///
656/// Reference: Modest, "Radiative Heat Transfer", 3rd ed.
657#[allow(dead_code)]
658#[derive(Debug, Clone)]
659pub struct ParticipatingMedia {
660    /// Absorption coefficient κ \[1/m\].
661    pub kappa: f64,
662    /// Scattering coefficient σ_s \[1/m\].
663    pub sigma_s: f64,
664    /// Temperature of the medium \[K\].
665    pub temperature: f64,
666}
667impl ParticipatingMedia {
668    /// Create a new participating media model.
669    pub fn new(kappa: f64, sigma_s: f64, temperature: f64) -> Self {
670        Self {
671            kappa,
672            sigma_s,
673            temperature,
674        }
675    }
676    /// Extinction coefficient β = κ + σ_s \[1/m\].
677    pub fn extinction(&self) -> f64 {
678        self.kappa + self.sigma_s
679    }
680    /// Single-scattering albedo ω = σ_s / β.
681    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    /// Optical depth over a path length L: τ = β · L.
689    pub fn optical_depth(&self, path_length: f64) -> f64 {
690        self.extinction() * path_length
691    }
692    /// Transmittance through a slab of thickness L: T = exp(-τ).
693    pub fn transmittance(&self, path_length: f64) -> f64 {
694        (-self.optical_depth(path_length)).exp()
695    }
696    /// Emitted radiation from a gas column of thickness L \[W/m²\].
697    ///
698    /// q_emit = κ · σ · T⁴ · L  (optically thin approximation)
699    pub fn emission_optically_thin(&self, path_length: f64) -> f64 {
700        self.kappa * SIGMA * self.temperature.powi(4) * path_length
701    }
702    /// Effective emissivity of a gas layer of thickness L.
703    ///
704    /// ε_eff = 1 - exp(-κ·L)  (gray gas approximation)
705    pub fn effective_emissivity(&self, path_length: f64) -> f64 {
706        1.0 - (-self.kappa * path_length).exp()
707    }
708    /// Mean-beam length for a sphere of radius R: L_m = 0.65 * 2R.
709    pub fn mean_beam_length_sphere(radius: f64) -> f64 {
710        0.65 * 2.0 * radius
711    }
712    /// Mean-beam length for a cylinder (infinite, radius R): L_m = 0.95 * 2R.
713    pub fn mean_beam_length_cylinder(radius: f64) -> f64 {
714        0.95 * 2.0 * radius
715    }
716}
717/// Nuclear radiation damage model.
718///
719/// Computes the displacement-per-atom (dpa) dose from fluence and cross-section.
720#[derive(Debug, Clone, Copy)]
721#[allow(dead_code)]
722pub struct RadiationDamage {
723    /// Neutron fluence \[n/cm²\]
724    pub fluence: f64,
725    /// Displacement cross-section σ_d \[cm²\]
726    pub displacement_cross_section: f64,
727    /// Atomic number density N \[atoms/cm³\]
728    pub atomic_density: f64,
729}
730impl RadiationDamage {
731    /// Create a new `RadiationDamage` model.
732    #[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    /// Displacements per atom (dpa).
741    ///
742    /// `dpa = fluence · σ_d · N / N = fluence · σ_d`
743    /// (simplified NRT model, one dpa per unit fluence × cross-section)
744    #[allow(dead_code)]
745    pub fn dpa(&self) -> f64 {
746        self.fluence * self.displacement_cross_section
747    }
748    /// Fraction of atoms displaced (saturates at ~1).
749    #[allow(dead_code)]
750    pub fn displaced_fraction(&self) -> f64 {
751        self.dpa().min(1.0)
752    }
753    /// Effective swelling model \[dimensionless volume increase / volume\].
754    ///
755    /// Simple empirical relation: `ΔV/V = A · dpa^n`, with `A = 1e-3`, `n = 2`.
756    #[allow(dead_code)]
757    pub fn swelling_fraction(&self) -> f64 {
758        let dpa = self.dpa();
759        1.0e-3 * dpa * dpa
760    }
761}
762/// Gray body model with temperature-dependent emissivity.
763///
764/// Represents a real surface whose emissivity varies with temperature using
765/// a linear model: `ε(T) = ε₀ + dε/dT · (T - T_ref)`.
766#[derive(Debug, Clone, Copy)]
767#[allow(dead_code)]
768pub struct GrayBodyModel {
769    /// Emissivity at reference temperature
770    pub eps_ref: f64,
771    /// Rate of change of emissivity with temperature \[1/K\]
772    pub deps_dt: f64,
773    /// Reference temperature \[K\]
774    pub temp_ref: f64,
775}
776impl GrayBodyModel {
777    /// Create a new gray body model.
778    #[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    /// Emissivity at temperature `temp_k`.
787    #[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    /// Emissive power \[W/m²\] at temperature `temp_k`.
792    #[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    /// Effective temperature that a blackbody would need to emit the same
797    /// total power as this gray body at `temp_k`.
798    ///
799    /// `T_eff = T * ε^0.25`
800    #[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/// Simple 1-D Monte Carlo radiation transport through an absorbing/scattering slab.
807///
808/// Uses path-length sampling to simulate photon transport through a homogeneous
809/// participating medium of thickness `L` with extinction coefficient `β = κ + σ_s`.
810#[derive(Debug, Clone)]
811#[allow(dead_code)]
812pub struct MCRadiationTransport {
813    /// Number of photon packets.
814    pub n_photons: usize,
815    /// LCG seed.
816    pub(super) seed: u64,
817}
818impl MCRadiationTransport {
819    /// Create a new MC transport calculator.
820    #[allow(dead_code)]
821    pub fn new(n_photons: usize, seed: u64) -> Self {
822        Self { n_photons, seed }
823    }
824    /// Advance the LCG.
825    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    /// Estimate slab transmittance for a purely absorbing medium.
833    ///
834    /// # Arguments
835    /// * `kappa` — absorption coefficient \[1/m\]
836    /// * `thickness` — slab thickness \[m\]
837    #[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    /// Estimate slab transmittance for an absorbing + scattering medium.
842    ///
843    /// # Arguments
844    /// * `kappa`     — absorption coefficient \[1/m\]
845    /// * `thickness` — slab thickness \[m\]
846    /// * `albedo`    — single-scattering albedo ω = σ_s / (κ + σ_s)
847    #[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    /// Estimate the mean free path \[m\] in a medium with extinction coefficient `beta`.
888    ///
889    /// Analytical result: `mfp = 1/beta`. This method provides the MC estimate.
890    #[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}