#![allow(clippy::needless_range_loop)]
use eulumdat_goniosim::*;
#[test]
fn isotropic_free_space() {
let scene = bare_isotropic(1000.0);
let config = TracerConfig {
num_photons: 500_000,
detector_c_resolution: 10.0,
detector_g_resolution: 5.0,
seed: 42,
..TracerConfig::default()
};
let result = Tracer::trace(&scene, &config);
assert_eq!(
result.stats.photons_detected, result.stats.photons_traced,
"All photons should escape in free space"
);
let energy_ratio = result.stats.total_energy_detected / result.stats.total_energy_emitted;
assert!(
(energy_ratio - 1.0).abs() < 0.001,
"Energy conservation violated: ratio = {energy_ratio}"
);
let candela = result.detector.to_candela(1000.0);
let expected_cd = 1000.0 / (4.0 * std::f64::consts::PI);
let mut sum_sq_error = 0.0;
let mut count = 0;
for ci in 0..candela.len() {
for gi in 1..candela[ci].len() - 1 {
let cd = candela[ci][gi];
if cd > 0.0 {
let err = (cd - expected_cd) / expected_cd;
sum_sq_error += err * err;
count += 1;
}
}
}
let rms_error = (sum_sq_error / count as f64).sqrt();
assert!(
rms_error < 0.10,
"RMS error {rms_error:.4} exceeds 10% for isotropic source at 500k photons"
);
}
#[test]
fn lambertian_free_space() {
let scene = bare_lambertian(1000.0);
let config = TracerConfig {
num_photons: 500_000,
detector_c_resolution: 10.0,
detector_g_resolution: 5.0,
seed: 123,
..TracerConfig::default()
};
let result = Tracer::trace(&scene, &config);
assert!(
result.stats.photons_detected > 0,
"Should detect some photons"
);
let candela = result.detector.to_candela(1000.0);
let mut max_cd = 0.0f64;
for ci in 0..candela.len() {
max_cd = max_cd.max(candela[ci][0]);
}
let gi_60 = 12; let mut sum_60 = 0.0;
let mut n_60 = 0;
for ci in 0..candela.len() {
if candela[ci][gi_60] > 0.0 {
sum_60 += candela[ci][gi_60];
n_60 += 1;
}
}
if n_60 > 0 && max_cd > 0.0 {
let ratio_60 = (sum_60 / n_60 as f64) / max_cd;
let expected_ratio = 60.0f64.to_radians().cos(); let error = (ratio_60 - expected_ratio).abs();
assert!(
error < 0.25,
"Lambertian at 60deg: ratio={ratio_60:.3}, expected={expected_ratio:.3}, error={error:.3}"
);
}
}
#[test]
fn export_roundtrip() {
let scene = bare_isotropic(1000.0);
let config = TracerConfig {
num_photons: 100_000,
detector_c_resolution: 15.0,
detector_g_resolution: 5.0,
seed: 99,
..TracerConfig::default()
};
let result = Tracer::trace(&scene, &config);
let export_config = ExportConfig {
c_step_deg: 15.0,
g_step_deg: 5.0,
luminaire_name: "Test Isotropic".to_string(),
..ExportConfig::default()
};
let ldt = detector_to_eulumdat(&result.detector, 1000.0, &export_config);
assert_eq!(ldt.luminaire_name, "Test Isotropic");
assert!(!ldt.c_angles.is_empty());
assert!(!ldt.g_angles.is_empty());
assert!(!ldt.intensities.is_empty());
let ldt_string = ldt.to_ldt();
assert!(!ldt_string.is_empty());
let parsed = eulumdat::Eulumdat::parse(&ldt_string).expect("Should parse exported LDT");
assert_eq!(parsed.luminaire_name, "Test Isotropic");
assert_eq!(parsed.c_angles.len(), ldt.c_angles.len());
assert_eq!(parsed.g_angles.len(), ldt.g_angles.len());
}
#[test]
fn scene_builder_led_housing_cover() {
let scene = SceneBuilder::new()
.source(Source::Led {
position: nalgebra::Point3::origin(),
direction: nalgebra::Unit::new_unchecked(nalgebra::Vector3::new(0.0, 0.0, -1.0)),
half_angle_deg: 60.0,
flux_lm: 1000.0,
})
.reflector(
catalog::anodized_aluminum(),
ReflectorPlacement {
distance_mm: 25.0,
length_mm: 50.0,
side: ReflectorSide::Surround,
},
)
.cover(
catalog::opal_pmma_3mm(),
CoverPlacement {
distance_mm: 40.0,
width_mm: 60.0,
height_mm: 60.0,
},
)
.build();
assert_eq!(scene.sources.len(), 1);
assert_eq!(scene.objects.len(), 2);
let config = TracerConfig {
num_photons: 10_000,
seed: 7,
..TracerConfig::default()
};
let result = Tracer::trace(&scene, &config);
assert!(result.stats.photons_traced == 10_000);
assert!(
result.stats.photons_detected > 0,
"Some photons should escape"
);
}
#[test]
fn catalog_all_materials_convert() {
let catalog = material_catalog();
assert!(catalog.len() >= 12, "Should have at least 12 materials");
for mat in &catalog {
let _internal = mat.to_material();
}
}