eulumdat_goniosim/
export.rs1use crate::detector::Detector;
4use eulumdat::{Eulumdat, LampSet, Symmetry};
5
6#[derive(Debug, Clone)]
8pub struct ExportConfig {
9 pub c_step_deg: f64,
11 pub g_step_deg: f64,
13 pub symmetry: Option<Symmetry>,
15 pub luminaire_name: String,
17 pub manufacturer: String,
19 pub luminaire_dimensions_mm: (f64, f64, f64),
21 pub luminous_area_mm: (f64, f64),
23}
24
25impl Default for ExportConfig {
26 fn default() -> Self {
27 Self {
28 c_step_deg: 15.0,
29 g_step_deg: 5.0,
30 symmetry: None,
31 luminaire_name: "Simulated Luminaire".to_string(),
32 manufacturer: "eulumdat-goniosim".to_string(),
33 luminaire_dimensions_mm: (100.0, 100.0, 50.0),
34 luminous_area_mm: (80.0, 80.0),
35 }
36 }
37}
38
39pub fn detector_to_eulumdat(
51 detector: &Detector,
52 source_flux_lm: f64,
53 config: &ExportConfig,
54) -> Eulumdat {
55 detector_to_eulumdat_with_lamp_flux(detector, source_flux_lm, source_flux_lm, config)
56}
57
58pub fn detector_to_eulumdat_with_lamp_flux(
60 detector: &Detector,
61 source_flux_lm: f64,
62 lamp_flux_lm: f64,
63 config: &ExportConfig,
64) -> Eulumdat {
65 detector_to_eulumdat_at_angles(
66 detector,
67 source_flux_lm,
68 lamp_flux_lm,
69 None, None,
71 config,
72 )
73}
74
75#[allow(clippy::needless_range_loop)]
80pub fn detector_to_eulumdat_at_angles(
81 detector: &Detector,
82 source_flux_lm: f64,
83 lamp_flux_lm: f64,
84 c_angles_opt: Option<&[f64]>,
85 g_angles_opt: Option<&[f64]>,
86 config: &ExportConfig,
87) -> Eulumdat {
88 let c_angles: Vec<f64> = match c_angles_opt {
90 Some(angles) => angles.to_vec(),
91 None => {
92 let num_c = (360.0 / config.c_step_deg).round() as usize;
93 (0..num_c).map(|i| i as f64 * config.c_step_deg).collect()
94 }
95 };
96
97 let g_angles: Vec<f64> = match g_angles_opt {
98 Some(angles) => angles.to_vec(),
99 None => {
100 let num_g = (180.0 / config.g_step_deg).round() as usize + 1;
101 (0..num_g).map(|i| i as f64 * config.g_step_deg).collect()
102 }
103 };
104
105 let num_c = c_angles.len();
106 let num_g = g_angles.len();
107
108 let scale = 1000.0 / lamp_flux_lm.max(1.0);
113
114 let intensities: Vec<Vec<f64>> = if c_angles_opt.is_some() || g_angles_opt.is_some() {
115 let full_cd = detector.to_candela(source_flux_lm);
117 let det_c_res = detector.c_resolution_deg();
118 let det_g_res = detector.g_resolution_deg();
119 let det_num_c = detector.num_c();
120 let det_num_g = detector.num_g();
121
122 c_angles
123 .iter()
124 .map(|&c| {
125 let c_norm = c.rem_euclid(360.0);
127 let ci = ((c_norm / det_c_res).round() as usize).min(det_num_c - 1);
128 g_angles
129 .iter()
130 .map(|&g| {
131 let gi = ((g / det_g_res).round() as usize).min(det_num_g - 1);
132 full_cd[ci][gi] * scale
133 })
134 .collect()
135 })
136 .collect()
137 } else {
138 let resampled = detector.resample(config.c_step_deg, config.g_step_deg);
140 let candela = resampled.to_candela(source_flux_lm);
141 candela
142 .iter()
143 .map(|c_plane| c_plane.iter().map(|cd| cd * scale).collect())
144 .collect()
145 };
146
147 let downward_energy: f64 = {
149 let det_bins = detector.bins();
150 let mut down = 0.0;
151 let mut total = 0.0;
152 for ci in 0..detector.num_c() {
153 for gi in 0..detector.num_g() {
154 let g_deg = gi as f64 * detector.g_resolution_deg();
155 let e = det_bins[ci][gi];
156 total += e;
157 if g_deg <= 90.0 {
158 down += e;
159 }
160 }
161 }
162 if total > 0.0 {
163 100.0 * down / total
164 } else {
165 50.0
166 }
167 };
168
169 let symmetry = config.symmetry.unwrap_or(Symmetry::None);
172
173 let (c_angles, intensities) = match symmetry {
174 Symmetry::VerticalAxis => {
175 let mut avg = vec![0.0; num_g];
178 for gi in 0..num_g {
179 let sum: f64 = intensities.iter().map(|cp| cp[gi]).sum();
180 avg[gi] = sum / num_c as f64;
181 }
182 (vec![0.0], vec![avg])
183 }
184 _ => (c_angles, intensities),
185 };
186 let num_c = c_angles.len();
187
188 let mut ldt = Eulumdat::new();
189 ldt.identification = config.manufacturer.clone();
190 ldt.luminaire_name = config.luminaire_name.clone();
191 ldt.luminaire_number = String::new();
192 ldt.file_name = String::new();
193 ldt.date_user = String::new();
194 ldt.measurement_report_number = "GonioSim".to_string();
195
196 ldt.symmetry = symmetry;
197 ldt.num_c_planes = num_c;
198 ldt.c_plane_distance = if c_angles_opt.is_some() && num_c > 1 {
200 let d = c_angles[1] - c_angles[0];
201 if c_angles.windows(2).all(|w| (w[1] - w[0] - d).abs() < 0.01) {
202 d
203 } else {
204 0.0
205 }
206 } else {
207 config.c_step_deg
208 };
209 ldt.num_g_planes = num_g;
210 ldt.g_plane_distance = if g_angles_opt.is_some() && num_g > 1 {
211 let d = g_angles[1] - g_angles[0];
212 if g_angles.windows(2).all(|w| (w[1] - w[0] - d).abs() < 0.01) {
213 d
214 } else {
215 0.0
216 }
217 } else {
218 config.g_step_deg
219 };
220
221 ldt.length = config.luminaire_dimensions_mm.0;
222 ldt.width = config.luminaire_dimensions_mm.1;
223 ldt.height = config.luminaire_dimensions_mm.2;
224 ldt.luminous_area_length = config.luminous_area_mm.0;
225 ldt.luminous_area_width = config.luminous_area_mm.1;
226
227 ldt.downward_flux_fraction = downward_energy;
228 ldt.light_output_ratio = 100.0; ldt.conversion_factor = 1.0;
230 ldt.tilt_angle = 0.0;
231
232 ldt.lamp_sets = vec![LampSet {
233 num_lamps: 1,
234 lamp_type: "LED".to_string(),
235 total_luminous_flux: source_flux_lm,
236 color_appearance: "4000K".to_string(),
237 color_rendering_group: "1A".to_string(),
238 wattage_with_ballast: source_flux_lm / 150.0, }];
240
241 ldt.direct_ratios = [0.0; 10];
242 ldt.c_angles = c_angles;
243 ldt.g_angles = g_angles;
244 ldt.intensities = intensities;
245
246 ldt
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn export_produces_valid_ldt() {
255 let mut detector = Detector::new(15.0, 5.0);
256 for ci in 0..detector.num_c() {
258 for gi in 0..detector.num_g() {
259 let dir = cg_to_direction(ci as f64 * 15.0, gi as f64 * 5.0);
260 detector.record(&dir, 1.0);
261 }
262 }
263
264 let config = ExportConfig::default();
265 let ldt = detector_to_eulumdat(&detector, 1000.0, &config);
266
267 assert_eq!(ldt.luminaire_name, "Simulated Luminaire");
268 assert!(!ldt.intensities.is_empty());
269 assert!(!ldt.c_angles.is_empty());
270 assert!(!ldt.g_angles.is_empty());
271
272 let ldt_string = ldt.to_ldt();
274 assert!(!ldt_string.is_empty());
275 }
276
277 fn cg_to_direction(c_deg: f64, g_deg: f64) -> nalgebra::Vector3<f64> {
279 let g_rad = g_deg.to_radians();
280 let c_rad = c_deg.to_radians();
281 nalgebra::Vector3::new(
282 g_rad.sin() * c_rad.cos(),
283 g_rad.sin() * c_rad.sin(),
284 -g_rad.cos(),
285 )
286 }
287}