1use pyo3::prelude::*;
4use pyo3::types::PyDict;
5
6use ::eulumdat as core;
7use core::{
8 CieFluxCodes as CoreCieFluxCodes, GldfPhotometricData as CoreGldfPhotometricData,
9 PhotometricCalculations as CoreCalcs, PhotometricSummary as CorePhotoSummary,
10 UgrParams as CoreUgrParams, ZonalLumens30 as CoreZonalLumens30,
11};
12
13#[pyclass]
22#[derive(Clone, Debug)]
23pub struct CieFluxCodes {
24 #[pyo3(get)]
26 pub n1: f64,
27 #[pyo3(get)]
29 pub n2: f64,
30 #[pyo3(get)]
32 pub n3: f64,
33 #[pyo3(get)]
35 pub n4: f64,
36 #[pyo3(get)]
38 pub n5: f64,
39}
40
41#[pymethods]
42impl CieFluxCodes {
43 fn __str__(&self) -> String {
45 format!(
46 "{:.0} {:.0} {:.0} {:.0} {:.0}",
47 self.n1.round(),
48 self.n2.round(),
49 self.n3.round(),
50 self.n4.round(),
51 self.n5.round()
52 )
53 }
54
55 fn __repr__(&self) -> String {
56 format!(
57 "CieFluxCodes(n1={:.1}, n2={:.1}, n3={:.1}, n4={:.1}, n5={:.1})",
58 self.n1, self.n2, self.n3, self.n4, self.n5
59 )
60 }
61
62 fn to_dict<'py>(&self, py: Python<'py>) -> Bound<'py, PyDict> {
64 let dict = PyDict::new(py);
65 dict.set_item("n1", self.n1).unwrap();
66 dict.set_item("n2", self.n2).unwrap();
67 dict.set_item("n3", self.n3).unwrap();
68 dict.set_item("n4", self.n4).unwrap();
69 dict.set_item("n5", self.n5).unwrap();
70 dict
71 }
72}
73
74impl From<CoreCieFluxCodes> for CieFluxCodes {
75 fn from(c: CoreCieFluxCodes) -> Self {
76 Self {
77 n1: c.n1,
78 n2: c.n2,
79 n3: c.n3,
80 n4: c.n4,
81 n5: c.n5,
82 }
83 }
84}
85
86#[pyclass]
90#[derive(Clone, Debug)]
91pub struct ZonalLumens30 {
92 #[pyo3(get)]
94 pub zone_0_30: f64,
95 #[pyo3(get)]
97 pub zone_30_60: f64,
98 #[pyo3(get)]
100 pub zone_60_90: f64,
101 #[pyo3(get)]
103 pub zone_90_120: f64,
104 #[pyo3(get)]
106 pub zone_120_150: f64,
107 #[pyo3(get)]
109 pub zone_150_180: f64,
110}
111
112#[pymethods]
113impl ZonalLumens30 {
114 fn downward_total(&self) -> f64 {
116 self.zone_0_30 + self.zone_30_60 + self.zone_60_90
117 }
118
119 fn upward_total(&self) -> f64 {
121 self.zone_90_120 + self.zone_120_150 + self.zone_150_180
122 }
123
124 fn __repr__(&self) -> String {
125 format!(
126 "ZonalLumens30(down={:.1}%, up={:.1}%)",
127 self.downward_total(),
128 self.upward_total()
129 )
130 }
131
132 fn to_dict<'py>(&self, py: Python<'py>) -> Bound<'py, PyDict> {
134 let dict = PyDict::new(py);
135 dict.set_item("zone_0_30", self.zone_0_30).unwrap();
136 dict.set_item("zone_30_60", self.zone_30_60).unwrap();
137 dict.set_item("zone_60_90", self.zone_60_90).unwrap();
138 dict.set_item("zone_90_120", self.zone_90_120).unwrap();
139 dict.set_item("zone_120_150", self.zone_120_150).unwrap();
140 dict.set_item("zone_150_180", self.zone_150_180).unwrap();
141 dict
142 }
143}
144
145impl From<CoreZonalLumens30> for ZonalLumens30 {
146 fn from(z: CoreZonalLumens30) -> Self {
147 Self {
148 zone_0_30: z.zone_0_30,
149 zone_30_60: z.zone_30_60,
150 zone_60_90: z.zone_60_90,
151 zone_90_120: z.zone_90_120,
152 zone_120_150: z.zone_120_150,
153 zone_150_180: z.zone_150_180,
154 }
155 }
156}
157
158#[pyclass]
163#[derive(Clone, Debug)]
164pub struct PhotometricSummary {
165 #[pyo3(get)]
168 pub total_lamp_flux: f64,
169 #[pyo3(get)]
171 pub calculated_flux: f64,
172 #[pyo3(get)]
174 pub lor: f64,
175 #[pyo3(get)]
177 pub dlor: f64,
178 #[pyo3(get)]
180 pub ulor: f64,
181
182 #[pyo3(get)]
185 pub lamp_efficacy: f64,
186 #[pyo3(get)]
188 pub luminaire_efficacy: f64,
189 #[pyo3(get)]
191 pub total_wattage: f64,
192
193 #[pyo3(get)]
196 pub beam_angle: f64,
197 #[pyo3(get)]
199 pub field_angle: f64,
200
201 #[pyo3(get)]
204 pub max_intensity: f64,
205 #[pyo3(get)]
207 pub min_intensity: f64,
208 #[pyo3(get)]
210 pub avg_intensity: f64,
211
212 #[pyo3(get)]
215 pub spacing_c0: f64,
216 #[pyo3(get)]
218 pub spacing_c90: f64,
219
220 inner_cie_codes: CoreCieFluxCodes,
222 inner_zonal_lumens: CoreZonalLumens30,
223}
224
225#[pymethods]
226impl PhotometricSummary {
227 #[getter]
229 fn cie_flux_codes(&self) -> CieFluxCodes {
230 self.inner_cie_codes.into()
231 }
232
233 #[getter]
235 fn zonal_lumens(&self) -> ZonalLumens30 {
236 self.inner_zonal_lumens.into()
237 }
238
239 fn to_text(&self) -> String {
241 format!(
242 r#"PHOTOMETRIC SUMMARY
243==================
244
245Luminous Flux
246 Total Lamp Flux: {:.0} lm
247 Calculated Flux: {:.0} lm
248 LOR: {:.1}%
249 DLOR / ULOR: {:.1}% / {:.1}%
250
251Efficacy
252 Lamp Efficacy: {:.1} lm/W
253 Luminaire Efficacy: {:.1} lm/W
254 Total Wattage: {:.1} W
255
256CIE Flux Code: {}
257
258Beam Characteristics
259 Beam Angle (50%): {:.1}°
260 Field Angle (10%): {:.1}°
261
262Intensity (cd/klm)
263 Maximum: {:.1}
264 Minimum: {:.1}
265 Average: {:.1}
266
267Spacing Criterion (S/H)
268 C0 Plane: {:.2}
269 C90 Plane: {:.2}
270
271Zonal Lumens (%)
272 0-30°: {:.1}%
273 30-60°: {:.1}%
274 60-90°: {:.1}%
275 90-120°: {:.1}%
276 120-150°: {:.1}%
277 150-180°: {:.1}%
278"#,
279 self.total_lamp_flux,
280 self.calculated_flux,
281 self.lor,
282 self.dlor,
283 self.ulor,
284 self.lamp_efficacy,
285 self.luminaire_efficacy,
286 self.total_wattage,
287 self.inner_cie_codes,
288 self.beam_angle,
289 self.field_angle,
290 self.max_intensity,
291 self.min_intensity,
292 self.avg_intensity,
293 self.spacing_c0,
294 self.spacing_c90,
295 self.inner_zonal_lumens.zone_0_30,
296 self.inner_zonal_lumens.zone_30_60,
297 self.inner_zonal_lumens.zone_60_90,
298 self.inner_zonal_lumens.zone_90_120,
299 self.inner_zonal_lumens.zone_120_150,
300 self.inner_zonal_lumens.zone_150_180,
301 )
302 }
303
304 fn to_compact(&self) -> String {
306 format!(
307 "CIE:{} Beam:{:.0}° Field:{:.0}° Eff:{:.0}lm/W S/H:{:.1}×{:.1}",
308 self.inner_cie_codes,
309 self.beam_angle,
310 self.field_angle,
311 self.luminaire_efficacy,
312 self.spacing_c0,
313 self.spacing_c90,
314 )
315 }
316
317 fn __str__(&self) -> String {
318 self.to_text()
319 }
320
321 fn __repr__(&self) -> String {
322 format!(
323 "PhotometricSummary(flux={:.0}lm, beam={:.1}°, field={:.1}°, eff={:.1}lm/W)",
324 self.total_lamp_flux, self.beam_angle, self.field_angle, self.luminaire_efficacy
325 )
326 }
327
328 fn to_dict<'py>(&self, py: Python<'py>) -> Bound<'py, PyDict> {
330 let dict = PyDict::new(py);
331 dict.set_item("total_lamp_flux", self.total_lamp_flux)
332 .unwrap();
333 dict.set_item("calculated_flux", self.calculated_flux)
334 .unwrap();
335 dict.set_item("lor", self.lor).unwrap();
336 dict.set_item("dlor", self.dlor).unwrap();
337 dict.set_item("ulor", self.ulor).unwrap();
338 dict.set_item("lamp_efficacy", self.lamp_efficacy).unwrap();
339 dict.set_item("luminaire_efficacy", self.luminaire_efficacy)
340 .unwrap();
341 dict.set_item("total_wattage", self.total_wattage).unwrap();
342 dict.set_item("beam_angle", self.beam_angle).unwrap();
343 dict.set_item("field_angle", self.field_angle).unwrap();
344 dict.set_item("max_intensity", self.max_intensity).unwrap();
345 dict.set_item("min_intensity", self.min_intensity).unwrap();
346 dict.set_item("avg_intensity", self.avg_intensity).unwrap();
347 dict.set_item("spacing_c0", self.spacing_c0).unwrap();
348 dict.set_item("spacing_c90", self.spacing_c90).unwrap();
349 dict.set_item(
350 "cie_flux_code",
351 format!("{}", self.inner_cie_codes).as_str(),
352 )
353 .unwrap();
354 dict.set_item("cie_n1", self.inner_cie_codes.n1).unwrap();
355 dict.set_item("cie_n2", self.inner_cie_codes.n2).unwrap();
356 dict.set_item("cie_n3", self.inner_cie_codes.n3).unwrap();
357 dict.set_item("cie_n4", self.inner_cie_codes.n4).unwrap();
358 dict.set_item("cie_n5", self.inner_cie_codes.n5).unwrap();
359 dict.set_item("zonal_0_30", self.inner_zonal_lumens.zone_0_30)
360 .unwrap();
361 dict.set_item("zonal_30_60", self.inner_zonal_lumens.zone_30_60)
362 .unwrap();
363 dict.set_item("zonal_60_90", self.inner_zonal_lumens.zone_60_90)
364 .unwrap();
365 dict.set_item("zonal_90_120", self.inner_zonal_lumens.zone_90_120)
366 .unwrap();
367 dict.set_item("zonal_120_150", self.inner_zonal_lumens.zone_120_150)
368 .unwrap();
369 dict.set_item("zonal_150_180", self.inner_zonal_lumens.zone_150_180)
370 .unwrap();
371 dict
372 }
373}
374
375impl From<CorePhotoSummary> for PhotometricSummary {
376 fn from(s: CorePhotoSummary) -> Self {
377 Self {
378 total_lamp_flux: s.total_lamp_flux,
379 calculated_flux: s.calculated_flux,
380 lor: s.lor,
381 dlor: s.dlor,
382 ulor: s.ulor,
383 lamp_efficacy: s.lamp_efficacy,
384 luminaire_efficacy: s.luminaire_efficacy,
385 total_wattage: s.total_wattage,
386 beam_angle: s.beam_angle,
387 field_angle: s.field_angle,
388 max_intensity: s.max_intensity,
389 min_intensity: s.min_intensity,
390 avg_intensity: s.avg_intensity,
391 spacing_c0: s.spacing_c0,
392 spacing_c90: s.spacing_c90,
393 inner_cie_codes: s.cie_flux_codes,
394 inner_zonal_lumens: s.zonal_lumens,
395 }
396 }
397}
398
399#[pyclass]
404#[derive(Clone, Debug)]
405pub struct GldfPhotometricData {
406 #[pyo3(get)]
408 pub cie_flux_code: String,
409 #[pyo3(get)]
411 pub light_output_ratio: f64,
412 #[pyo3(get)]
414 pub luminous_efficacy: f64,
415 #[pyo3(get)]
417 pub downward_flux_fraction: f64,
418 #[pyo3(get)]
420 pub downward_light_output_ratio: f64,
421 #[pyo3(get)]
423 pub upward_light_output_ratio: f64,
424 #[pyo3(get)]
426 pub luminaire_luminance: f64,
427 #[pyo3(get)]
429 pub cut_off_angle: f64,
430 #[pyo3(get)]
432 pub photometric_code: String,
433 #[pyo3(get)]
435 pub half_peak_c0: f64,
436 #[pyo3(get)]
438 pub half_peak_c90: f64,
439 #[pyo3(get)]
441 pub tenth_peak_c0: f64,
442 #[pyo3(get)]
444 pub tenth_peak_c90: f64,
445 #[pyo3(get)]
447 pub bug_rating: String,
448 #[pyo3(get)]
450 pub ugr_crosswise: Option<f64>,
451 #[pyo3(get)]
453 pub ugr_endwise: Option<f64>,
454}
455
456#[pymethods]
457impl GldfPhotometricData {
458 fn to_text(&self) -> String {
460 let mut s = String::from("GLDF PHOTOMETRIC DATA\n");
461 s.push_str("=====================\n\n");
462
463 s.push_str(&format!(
464 "CIE Flux Code: {}\n",
465 self.cie_flux_code
466 ));
467 s.push_str(&format!(
468 "Light Output Ratio: {:.1}%\n",
469 self.light_output_ratio
470 ));
471 s.push_str(&format!(
472 "Luminous Efficacy: {:.1} lm/W\n",
473 self.luminous_efficacy
474 ));
475 s.push_str(&format!(
476 "Downward Flux Fraction: {:.1}%\n",
477 self.downward_flux_fraction
478 ));
479 s.push_str(&format!(
480 "DLOR: {:.1}%\n",
481 self.downward_light_output_ratio
482 ));
483 s.push_str(&format!(
484 "ULOR: {:.1}%\n",
485 self.upward_light_output_ratio
486 ));
487 s.push_str(&format!(
488 "Luminaire Luminance: {:.0} cd/m²\n",
489 self.luminaire_luminance
490 ));
491 s.push_str(&format!(
492 "Cut-off Angle: {:.1}°\n",
493 self.cut_off_angle
494 ));
495
496 if let (Some(cross), Some(end)) = (self.ugr_crosswise, self.ugr_endwise) {
497 s.push_str(&format!(
498 "UGR (4H×8H, 70/50/20): C: {:.1} / E: {:.1}\n",
499 cross, end
500 ));
501 }
502
503 s.push_str(&format!(
504 "Photometric Code: {}\n",
505 self.photometric_code
506 ));
507 s.push_str(&format!(
508 "Half Peak Divergence: {:.1}° / {:.1}° (C0/C90)\n",
509 self.half_peak_c0, self.half_peak_c90
510 ));
511 s.push_str(&format!(
512 "Tenth Peak Divergence: {:.1}° / {:.1}° (C0/C90)\n",
513 self.tenth_peak_c0, self.tenth_peak_c90
514 ));
515 s.push_str(&format!("BUG Rating: {}\n", self.bug_rating));
516
517 s
518 }
519
520 fn __str__(&self) -> String {
521 self.to_text()
522 }
523
524 fn __repr__(&self) -> String {
525 format!(
526 "GldfPhotometricData(cie='{}', lor={:.1}%, eff={:.1}lm/W, bug='{}')",
527 self.cie_flux_code, self.light_output_ratio, self.luminous_efficacy, self.bug_rating
528 )
529 }
530
531 fn to_dict<'py>(&self, py: Python<'py>) -> Bound<'py, PyDict> {
533 let dict = PyDict::new(py);
534 dict.set_item("cie_flux_code", &self.cie_flux_code).unwrap();
535 dict.set_item("light_output_ratio", self.light_output_ratio)
536 .unwrap();
537 dict.set_item("luminous_efficacy", self.luminous_efficacy)
538 .unwrap();
539 dict.set_item("downward_flux_fraction", self.downward_flux_fraction)
540 .unwrap();
541 dict.set_item(
542 "downward_light_output_ratio",
543 self.downward_light_output_ratio,
544 )
545 .unwrap();
546 dict.set_item("upward_light_output_ratio", self.upward_light_output_ratio)
547 .unwrap();
548 dict.set_item("luminaire_luminance", self.luminaire_luminance)
549 .unwrap();
550 dict.set_item("cut_off_angle", self.cut_off_angle).unwrap();
551 dict.set_item("photometric_code", &self.photometric_code)
552 .unwrap();
553 dict.set_item("half_peak_divergence_c0", self.half_peak_c0)
554 .unwrap();
555 dict.set_item("half_peak_divergence_c90", self.half_peak_c90)
556 .unwrap();
557 dict.set_item("tenth_peak_divergence_c0", self.tenth_peak_c0)
558 .unwrap();
559 dict.set_item("tenth_peak_divergence_c90", self.tenth_peak_c90)
560 .unwrap();
561 dict.set_item("bug_rating", &self.bug_rating).unwrap();
562 if let Some(ugr_c) = self.ugr_crosswise {
563 dict.set_item("ugr_crosswise", ugr_c).unwrap();
564 }
565 if let Some(ugr_e) = self.ugr_endwise {
566 dict.set_item("ugr_endwise", ugr_e).unwrap();
567 }
568 dict
569 }
570}
571
572impl From<CoreGldfPhotometricData> for GldfPhotometricData {
573 fn from(g: CoreGldfPhotometricData) -> Self {
574 Self {
575 cie_flux_code: g.cie_flux_code,
576 light_output_ratio: g.light_output_ratio,
577 luminous_efficacy: g.luminous_efficacy,
578 downward_flux_fraction: g.downward_flux_fraction,
579 downward_light_output_ratio: g.downward_light_output_ratio,
580 upward_light_output_ratio: g.upward_light_output_ratio,
581 luminaire_luminance: g.luminaire_luminance,
582 cut_off_angle: g.cut_off_angle,
583 photometric_code: g.photometric_code,
584 half_peak_c0: g.half_peak_divergence.0,
585 half_peak_c90: g.half_peak_divergence.1,
586 tenth_peak_c0: g.tenth_peak_divergence.0,
587 tenth_peak_c90: g.tenth_peak_divergence.1,
588 bug_rating: g.light_distribution_bug_rating,
589 ugr_crosswise: g.ugr_4h_8h_705020.as_ref().map(|u| u.crosswise),
590 ugr_endwise: g.ugr_4h_8h_705020.as_ref().map(|u| u.endwise),
591 }
592 }
593}
594
595#[pyclass]
597#[derive(Clone, Debug)]
598pub struct UgrParams {
599 #[pyo3(get, set)]
601 pub room_length: f64,
602 #[pyo3(get, set)]
604 pub room_width: f64,
605 #[pyo3(get, set)]
607 pub mounting_height: f64,
608 #[pyo3(get, set)]
610 pub eye_height: f64,
611 #[pyo3(get, set)]
613 pub observer_x: f64,
614 #[pyo3(get, set)]
616 pub observer_y: f64,
617 #[pyo3(get, set)]
619 pub rho_ceiling: f64,
620 #[pyo3(get, set)]
622 pub rho_wall: f64,
623 #[pyo3(get, set)]
625 pub rho_floor: f64,
626 #[pyo3(get, set)]
628 pub illuminance: f64,
629 luminaire_positions: Vec<(f64, f64)>,
631}
632
633#[pymethods]
634impl UgrParams {
635 #[new]
637 #[pyo3(signature = (room_length=8.0, room_width=4.0, mounting_height=2.8, eye_height=1.2, observer_x=4.0, observer_y=2.0))]
638 fn new(
639 room_length: f64,
640 room_width: f64,
641 mounting_height: f64,
642 eye_height: f64,
643 observer_x: f64,
644 observer_y: f64,
645 ) -> Self {
646 Self {
647 room_length,
648 room_width,
649 mounting_height,
650 eye_height,
651 observer_x,
652 observer_y,
653 luminaire_positions: vec![(2.0, 2.0), (6.0, 2.0)],
654 rho_ceiling: 0.7,
655 rho_wall: 0.5,
656 rho_floor: 0.2,
657 illuminance: 500.0,
658 }
659 }
660
661 #[staticmethod]
663 fn standard_office() -> Self {
664 let core = CoreUgrParams::standard_office();
665 Self::from(core)
666 }
667
668 #[getter]
670 fn get_luminaire_positions(&self) -> Vec<(f64, f64)> {
671 self.luminaire_positions.clone()
672 }
673
674 #[setter]
676 fn set_luminaire_positions(&mut self, positions: Vec<(f64, f64)>) {
677 self.luminaire_positions = positions;
678 }
679
680 fn add_luminaire(&mut self, x: f64, y: f64) {
682 self.luminaire_positions.push((x, y));
683 }
684
685 fn clear_luminaires(&mut self) {
687 self.luminaire_positions.clear();
688 }
689
690 fn __repr__(&self) -> String {
691 format!(
692 "UgrParams(room={:.1}×{:.1}m, h={:.1}m, {} luminaires)",
693 self.room_length,
694 self.room_width,
695 self.mounting_height,
696 self.luminaire_positions.len()
697 )
698 }
699}
700
701impl From<CoreUgrParams> for UgrParams {
702 fn from(p: CoreUgrParams) -> Self {
703 Self {
704 room_length: p.room_length,
705 room_width: p.room_width,
706 mounting_height: p.mounting_height,
707 eye_height: p.eye_height,
708 observer_x: p.observer_x,
709 observer_y: p.observer_y,
710 luminaire_positions: p.luminaire_positions,
711 rho_ceiling: p.rho_ceiling,
712 rho_wall: p.rho_wall,
713 rho_floor: p.rho_floor,
714 illuminance: p.illuminance,
715 }
716 }
717}
718
719impl From<&UgrParams> for CoreUgrParams {
720 fn from(p: &UgrParams) -> Self {
721 Self {
722 room_length: p.room_length,
723 room_width: p.room_width,
724 mounting_height: p.mounting_height,
725 eye_height: p.eye_height,
726 observer_x: p.observer_x,
727 observer_y: p.observer_y,
728 luminaire_positions: p.luminaire_positions.clone(),
729 rho_ceiling: p.rho_ceiling,
730 rho_wall: p.rho_wall,
731 rho_floor: p.rho_floor,
732 illuminance: p.illuminance,
733 }
734 }
735}
736
737pub struct PhotometricCalcs;
739
740impl PhotometricCalcs {
741 pub fn photometric_summary(ldt: &core::Eulumdat) -> PhotometricSummary {
742 CorePhotoSummary::from_eulumdat(ldt).into()
743 }
744
745 pub fn gldf_data(ldt: &core::Eulumdat) -> GldfPhotometricData {
746 CoreGldfPhotometricData::from_eulumdat(ldt).into()
747 }
748
749 pub fn cie_flux_codes(ldt: &core::Eulumdat) -> CieFluxCodes {
750 CoreCalcs::cie_flux_codes(ldt).into()
751 }
752
753 pub fn zonal_lumens_30(ldt: &core::Eulumdat) -> ZonalLumens30 {
754 CoreCalcs::zonal_lumens_30deg(ldt).into()
755 }
756
757 pub fn beam_angle(ldt: &core::Eulumdat) -> f64 {
758 CoreCalcs::beam_angle(ldt)
759 }
760
761 pub fn field_angle(ldt: &core::Eulumdat) -> f64 {
762 CoreCalcs::field_angle(ldt)
763 }
764
765 pub fn spacing_criteria(ldt: &core::Eulumdat) -> (f64, f64) {
766 CoreCalcs::spacing_criteria(ldt)
767 }
768
769 pub fn downward_flux(ldt: &core::Eulumdat, arc: f64) -> f64 {
770 CoreCalcs::downward_flux(ldt, arc)
771 }
772
773 pub fn cut_off_angle(ldt: &core::Eulumdat) -> f64 {
774 CoreCalcs::cut_off_angle(ldt)
775 }
776
777 pub fn photometric_code(ldt: &core::Eulumdat) -> String {
778 CoreCalcs::photometric_code(ldt)
779 }
780
781 pub fn luminaire_efficacy(ldt: &core::Eulumdat) -> f64 {
782 CoreCalcs::luminaire_efficacy(ldt)
783 }
784
785 pub fn ugr(ldt: &core::Eulumdat, params: &UgrParams) -> f64 {
786 let core_params = CoreUgrParams::from(params);
787 CoreCalcs::ugr(ldt, &core_params)
788 }
789}