1use pyo3::exceptions::PyValueError;
4use pyo3::prelude::*;
5
6use ::eulumdat as core;
7use core::{
8 diagram::{
9 ButterflyDiagram as CoreButterflyDiagram, CartesianDiagram as CoreCartesianDiagram,
10 HeatmapDiagram as CoreHeatmapDiagram, PolarDiagram as CorePolarDiagram,
11 },
12 BugDiagram as CoreBugDiagram, IesExporter, IesParser,
13};
14
15use crate::{
16 bug_rating::{BugRating, ZoneLumens},
17 calculations::{
18 CieFluxCodes, GldfPhotometricData, PhotometricCalcs, PhotometricSummary, UgrParams,
19 ZonalLumens30,
20 },
21 diagram::SvgTheme,
22 error::to_py_err,
23 validation::ValidationWarning,
24};
25
26#[pyclass(eq, eq_int)]
28#[derive(Clone, Copy, PartialEq, Eq)]
29pub enum TypeIndicator {
30 PointSourceSymmetric = 1,
32 Linear = 2,
34 PointSourceOther = 3,
36}
37
38#[pymethods]
39impl TypeIndicator {
40 #[staticmethod]
42 fn from_int(value: i32) -> PyResult<Self> {
43 match value {
44 1 => Ok(Self::PointSourceSymmetric),
45 2 => Ok(Self::Linear),
46 3 => Ok(Self::PointSourceOther),
47 _ => Err(PyValueError::new_err(format!(
48 "Invalid type indicator: {} (must be 1-3)",
49 value
50 ))),
51 }
52 }
53
54 fn as_int(&self) -> i32 {
56 *self as i32
57 }
58
59 fn __repr__(&self) -> String {
60 match self {
61 Self::PointSourceSymmetric => "TypeIndicator.PointSourceSymmetric".to_string(),
62 Self::Linear => "TypeIndicator.Linear".to_string(),
63 Self::PointSourceOther => "TypeIndicator.PointSourceOther".to_string(),
64 }
65 }
66}
67
68impl From<core::TypeIndicator> for TypeIndicator {
69 fn from(t: core::TypeIndicator) -> Self {
70 match t {
71 core::TypeIndicator::PointSourceSymmetric => Self::PointSourceSymmetric,
72 core::TypeIndicator::Linear => Self::Linear,
73 core::TypeIndicator::PointSourceOther => Self::PointSourceOther,
74 }
75 }
76}
77
78impl From<TypeIndicator> for core::TypeIndicator {
79 fn from(t: TypeIndicator) -> Self {
80 match t {
81 TypeIndicator::PointSourceSymmetric => Self::PointSourceSymmetric,
82 TypeIndicator::Linear => Self::Linear,
83 TypeIndicator::PointSourceOther => Self::PointSourceOther,
84 }
85 }
86}
87
88#[pyclass(eq, eq_int)]
90#[derive(Clone, Copy, PartialEq, Eq)]
91pub enum Symmetry {
92 None = 0,
94 VerticalAxis = 1,
96 PlaneC0C180 = 2,
98 PlaneC90C270 = 3,
100 BothPlanes = 4,
102}
103
104#[pymethods]
105impl Symmetry {
106 #[staticmethod]
108 fn from_int(value: i32) -> PyResult<Self> {
109 match value {
110 0 => Ok(Self::None),
111 1 => Ok(Self::VerticalAxis),
112 2 => Ok(Self::PlaneC0C180),
113 3 => Ok(Self::PlaneC90C270),
114 4 => Ok(Self::BothPlanes),
115 _ => Err(PyValueError::new_err(format!(
116 "Invalid symmetry: {} (must be 0-4)",
117 value
118 ))),
119 }
120 }
121
122 fn as_int(&self) -> i32 {
124 *self as i32
125 }
126
127 fn description(&self) -> &'static str {
129 match self {
130 Self::None => "no symmetry",
131 Self::VerticalAxis => "symmetry about the vertical axis",
132 Self::PlaneC0C180 => "symmetry to plane C0-C180",
133 Self::PlaneC90C270 => "symmetry to plane C90-C270",
134 Self::BothPlanes => "symmetry to plane C0-C180 and to plane C90-C270",
135 }
136 }
137
138 fn __repr__(&self) -> String {
139 match self {
140 Self::None => "Symmetry.None".to_string(),
141 Self::VerticalAxis => "Symmetry.VerticalAxis".to_string(),
142 Self::PlaneC0C180 => "Symmetry.PlaneC0C180".to_string(),
143 Self::PlaneC90C270 => "Symmetry.PlaneC90C270".to_string(),
144 Self::BothPlanes => "Symmetry.BothPlanes".to_string(),
145 }
146 }
147}
148
149impl From<core::Symmetry> for Symmetry {
150 fn from(s: core::Symmetry) -> Self {
151 match s {
152 core::Symmetry::None => Self::None,
153 core::Symmetry::VerticalAxis => Self::VerticalAxis,
154 core::Symmetry::PlaneC0C180 => Self::PlaneC0C180,
155 core::Symmetry::PlaneC90C270 => Self::PlaneC90C270,
156 core::Symmetry::BothPlanes => Self::BothPlanes,
157 }
158 }
159}
160
161impl From<Symmetry> for core::Symmetry {
162 fn from(s: Symmetry) -> Self {
163 match s {
164 Symmetry::None => Self::None,
165 Symmetry::VerticalAxis => Self::VerticalAxis,
166 Symmetry::PlaneC0C180 => Self::PlaneC0C180,
167 Symmetry::PlaneC90C270 => Self::PlaneC90C270,
168 Symmetry::BothPlanes => Self::BothPlanes,
169 }
170 }
171}
172
173#[pyclass]
175#[derive(Clone)]
176pub struct LampSet {
177 #[pyo3(get, set)]
179 pub num_lamps: i32,
180 #[pyo3(get, set)]
182 pub lamp_type: String,
183 #[pyo3(get, set)]
185 pub total_luminous_flux: f64,
186 #[pyo3(get, set)]
188 pub color_appearance: String,
189 #[pyo3(get, set)]
191 pub color_rendering_group: String,
192 #[pyo3(get, set)]
194 pub wattage_with_ballast: f64,
195}
196
197#[pymethods]
198impl LampSet {
199 #[new]
200 #[pyo3(signature = (num_lamps=1, lamp_type="".to_string(), total_luminous_flux=0.0, color_appearance="".to_string(), color_rendering_group="".to_string(), wattage_with_ballast=0.0))]
201 fn new(
202 num_lamps: i32,
203 lamp_type: String,
204 total_luminous_flux: f64,
205 color_appearance: String,
206 color_rendering_group: String,
207 wattage_with_ballast: f64,
208 ) -> Self {
209 Self {
210 num_lamps,
211 lamp_type,
212 total_luminous_flux,
213 color_appearance,
214 color_rendering_group,
215 wattage_with_ballast,
216 }
217 }
218
219 fn __repr__(&self) -> String {
220 format!(
221 "LampSet(num_lamps={}, lamp_type='{}', flux={:.1} lm, wattage={:.1} W)",
222 self.num_lamps, self.lamp_type, self.total_luminous_flux, self.wattage_with_ballast
223 )
224 }
225}
226
227impl From<&core::LampSet> for LampSet {
228 fn from(ls: &core::LampSet) -> Self {
229 Self {
230 num_lamps: ls.num_lamps,
231 lamp_type: ls.lamp_type.clone(),
232 total_luminous_flux: ls.total_luminous_flux,
233 color_appearance: ls.color_appearance.clone(),
234 color_rendering_group: ls.color_rendering_group.clone(),
235 wattage_with_ballast: ls.wattage_with_ballast,
236 }
237 }
238}
239
240impl From<&LampSet> for core::LampSet {
241 fn from(ls: &LampSet) -> Self {
242 Self {
243 num_lamps: ls.num_lamps,
244 lamp_type: ls.lamp_type.clone(),
245 total_luminous_flux: ls.total_luminous_flux,
246 color_appearance: ls.color_appearance.clone(),
247 color_rendering_group: ls.color_rendering_group.clone(),
248 wattage_with_ballast: ls.wattage_with_ballast,
249 }
250 }
251}
252
253#[pyclass]
257pub struct Eulumdat {
258 inner: core::Eulumdat,
259}
260
261#[pymethods]
262impl Eulumdat {
263 #[new]
265 fn new() -> Self {
266 Self {
267 inner: core::Eulumdat::new(),
268 }
269 }
270
271 #[staticmethod]
273 fn parse(content: &str) -> PyResult<Self> {
274 core::Eulumdat::parse(content)
275 .map(|inner| Self { inner })
276 .map_err(to_py_err)
277 }
278
279 #[staticmethod]
281 fn from_file(path: &str) -> PyResult<Self> {
282 core::Eulumdat::from_file(path)
283 .map(|inner| Self { inner })
284 .map_err(to_py_err)
285 }
286
287 #[staticmethod]
289 fn parse_ies(content: &str) -> PyResult<Self> {
290 IesParser::parse(content)
291 .map(|inner| Self { inner })
292 .map_err(to_py_err)
293 }
294
295 #[staticmethod]
297 fn from_ies_file(path: &str) -> PyResult<Self> {
298 IesParser::parse_file(path)
299 .map(|inner| Self { inner })
300 .map_err(to_py_err)
301 }
302
303 fn to_ldt(&self) -> String {
305 self.inner.to_ldt()
306 }
307
308 fn to_ies(&self) -> String {
310 IesExporter::export(&self.inner)
311 }
312
313 fn save(&self, path: &str) -> PyResult<()> {
315 self.inner.save(path).map_err(to_py_err)
316 }
317
318 fn validate(&self) -> Vec<ValidationWarning> {
320 self.inner
321 .validate()
322 .into_iter()
323 .map(|w| ValidationWarning {
324 code: w.code.to_string(),
325 message: w.message,
326 })
327 .collect()
328 }
329
330 #[getter]
334 fn identification(&self) -> &str {
335 &self.inner.identification
336 }
337
338 #[setter]
339 fn set_identification(&mut self, value: String) {
340 self.inner.identification = value;
341 }
342
343 #[getter]
347 fn type_indicator(&self) -> TypeIndicator {
348 self.inner.type_indicator.into()
349 }
350
351 #[setter]
352 fn set_type_indicator(&mut self, value: TypeIndicator) {
353 self.inner.type_indicator = value.into();
354 }
355
356 #[getter]
358 fn symmetry(&self) -> Symmetry {
359 self.inner.symmetry.into()
360 }
361
362 #[setter]
363 fn set_symmetry(&mut self, value: Symmetry) {
364 self.inner.symmetry = value.into();
365 }
366
367 #[getter]
371 fn num_c_planes(&self) -> usize {
372 self.inner.num_c_planes
373 }
374
375 #[setter]
376 fn set_num_c_planes(&mut self, value: usize) {
377 self.inner.num_c_planes = value;
378 }
379
380 #[getter]
382 fn c_plane_distance(&self) -> f64 {
383 self.inner.c_plane_distance
384 }
385
386 #[setter]
387 fn set_c_plane_distance(&mut self, value: f64) {
388 self.inner.c_plane_distance = value;
389 }
390
391 #[getter]
393 fn num_g_planes(&self) -> usize {
394 self.inner.num_g_planes
395 }
396
397 #[setter]
398 fn set_num_g_planes(&mut self, value: usize) {
399 self.inner.num_g_planes = value;
400 }
401
402 #[getter]
404 fn g_plane_distance(&self) -> f64 {
405 self.inner.g_plane_distance
406 }
407
408 #[setter]
409 fn set_g_plane_distance(&mut self, value: f64) {
410 self.inner.g_plane_distance = value;
411 }
412
413 #[getter]
417 fn measurement_report_number(&self) -> &str {
418 &self.inner.measurement_report_number
419 }
420
421 #[setter]
422 fn set_measurement_report_number(&mut self, value: String) {
423 self.inner.measurement_report_number = value;
424 }
425
426 #[getter]
428 fn luminaire_name(&self) -> &str {
429 &self.inner.luminaire_name
430 }
431
432 #[setter]
433 fn set_luminaire_name(&mut self, value: String) {
434 self.inner.luminaire_name = value;
435 }
436
437 #[getter]
439 fn luminaire_number(&self) -> &str {
440 &self.inner.luminaire_number
441 }
442
443 #[setter]
444 fn set_luminaire_number(&mut self, value: String) {
445 self.inner.luminaire_number = value;
446 }
447
448 #[getter]
450 fn file_name(&self) -> &str {
451 &self.inner.file_name
452 }
453
454 #[setter]
455 fn set_file_name(&mut self, value: String) {
456 self.inner.file_name = value;
457 }
458
459 #[getter]
461 fn date_user(&self) -> &str {
462 &self.inner.date_user
463 }
464
465 #[setter]
466 fn set_date_user(&mut self, value: String) {
467 self.inner.date_user = value;
468 }
469
470 #[getter]
474 fn length(&self) -> f64 {
475 self.inner.length
476 }
477
478 #[setter]
479 fn set_length(&mut self, value: f64) {
480 self.inner.length = value;
481 }
482
483 #[getter]
485 fn width(&self) -> f64 {
486 self.inner.width
487 }
488
489 #[setter]
490 fn set_width(&mut self, value: f64) {
491 self.inner.width = value;
492 }
493
494 #[getter]
496 fn height(&self) -> f64 {
497 self.inner.height
498 }
499
500 #[setter]
501 fn set_height(&mut self, value: f64) {
502 self.inner.height = value;
503 }
504
505 #[getter]
507 fn luminous_area_length(&self) -> f64 {
508 self.inner.luminous_area_length
509 }
510
511 #[setter]
512 fn set_luminous_area_length(&mut self, value: f64) {
513 self.inner.luminous_area_length = value;
514 }
515
516 #[getter]
518 fn luminous_area_width(&self) -> f64 {
519 self.inner.luminous_area_width
520 }
521
522 #[setter]
523 fn set_luminous_area_width(&mut self, value: f64) {
524 self.inner.luminous_area_width = value;
525 }
526
527 #[getter]
529 fn height_c0(&self) -> f64 {
530 self.inner.height_c0
531 }
532
533 #[setter]
534 fn set_height_c0(&mut self, value: f64) {
535 self.inner.height_c0 = value;
536 }
537
538 #[getter]
540 fn height_c90(&self) -> f64 {
541 self.inner.height_c90
542 }
543
544 #[setter]
545 fn set_height_c90(&mut self, value: f64) {
546 self.inner.height_c90 = value;
547 }
548
549 #[getter]
551 fn height_c180(&self) -> f64 {
552 self.inner.height_c180
553 }
554
555 #[setter]
556 fn set_height_c180(&mut self, value: f64) {
557 self.inner.height_c180 = value;
558 }
559
560 #[getter]
562 fn height_c270(&self) -> f64 {
563 self.inner.height_c270
564 }
565
566 #[setter]
567 fn set_height_c270(&mut self, value: f64) {
568 self.inner.height_c270 = value;
569 }
570
571 #[getter]
575 fn downward_flux_fraction(&self) -> f64 {
576 self.inner.downward_flux_fraction
577 }
578
579 #[setter]
580 fn set_downward_flux_fraction(&mut self, value: f64) {
581 self.inner.downward_flux_fraction = value;
582 }
583
584 #[getter]
586 fn light_output_ratio(&self) -> f64 {
587 self.inner.light_output_ratio
588 }
589
590 #[setter]
591 fn set_light_output_ratio(&mut self, value: f64) {
592 self.inner.light_output_ratio = value;
593 }
594
595 #[getter]
597 fn conversion_factor(&self) -> f64 {
598 self.inner.conversion_factor
599 }
600
601 #[setter]
602 fn set_conversion_factor(&mut self, value: f64) {
603 self.inner.conversion_factor = value;
604 }
605
606 #[getter]
608 fn tilt_angle(&self) -> f64 {
609 self.inner.tilt_angle
610 }
611
612 #[setter]
613 fn set_tilt_angle(&mut self, value: f64) {
614 self.inner.tilt_angle = value;
615 }
616
617 #[getter]
621 fn lamp_sets(&self) -> Vec<LampSet> {
622 self.inner.lamp_sets.iter().map(LampSet::from).collect()
623 }
624
625 #[setter]
626 fn set_lamp_sets(&mut self, value: Vec<LampSet>) {
627 self.inner.lamp_sets = value.iter().map(core::LampSet::from).collect();
628 }
629
630 #[getter]
634 fn direct_ratios(&self) -> Vec<f64> {
635 self.inner.direct_ratios.to_vec()
636 }
637
638 #[setter]
639 fn set_direct_ratios(&mut self, value: Vec<f64>) -> PyResult<()> {
640 if value.len() != 10 {
641 return Err(PyValueError::new_err(
642 "direct_ratios must have exactly 10 values",
643 ));
644 }
645 self.inner.direct_ratios.copy_from_slice(&value);
646 Ok(())
647 }
648
649 #[getter]
653 fn c_angles(&self) -> Vec<f64> {
654 self.inner.c_angles.clone()
655 }
656
657 #[setter]
658 fn set_c_angles(&mut self, value: Vec<f64>) {
659 self.inner.c_angles = value;
660 }
661
662 #[getter]
664 fn g_angles(&self) -> Vec<f64> {
665 self.inner.g_angles.clone()
666 }
667
668 #[setter]
669 fn set_g_angles(&mut self, value: Vec<f64>) {
670 self.inner.g_angles = value;
671 }
672
673 #[getter]
676 fn intensities(&self) -> Vec<Vec<f64>> {
677 self.inner.intensities.clone()
678 }
679
680 #[setter]
681 fn set_intensities(&mut self, value: Vec<Vec<f64>>) {
682 self.inner.intensities = value;
683 }
684
685 fn actual_c_planes(&self) -> usize {
689 self.inner.actual_c_planes()
690 }
691
692 fn total_luminous_flux(&self) -> f64 {
694 self.inner.total_luminous_flux()
695 }
696
697 fn total_wattage(&self) -> f64 {
699 self.inner.total_wattage()
700 }
701
702 fn luminous_efficacy(&self) -> f64 {
704 self.inner.luminous_efficacy()
705 }
706
707 fn max_intensity(&self) -> f64 {
709 self.inner.max_intensity()
710 }
711
712 fn min_intensity(&self) -> f64 {
714 self.inner.min_intensity()
715 }
716
717 fn avg_intensity(&self) -> f64 {
719 self.inner.avg_intensity()
720 }
721
722 fn get_intensity(&self, c_index: usize, g_index: usize) -> Option<f64> {
724 self.inner.get_intensity(c_index, g_index)
725 }
726
727 fn sample(&self, c_angle: f64, g_angle: f64) -> f64 {
739 self.inner.sample(c_angle, g_angle)
740 }
741
742 fn sample_normalized(&self, c_angle: f64, g_angle: f64) -> f64 {
753 let max = self.inner.max_intensity();
754 if max <= 0.0 {
755 return 0.0;
756 }
757 self.inner.sample(c_angle, g_angle) / max
758 }
759
760 #[pyo3(signature = (width=500.0, height=500.0, theme=SvgTheme::Light))]
764 fn polar_svg(&self, width: f64, height: f64, theme: SvgTheme) -> String {
765 let diagram = CorePolarDiagram::from_eulumdat(&self.inner);
766 diagram.to_svg(width, height, &theme.to_core())
767 }
768
769 #[pyo3(signature = (width=500.0, height=400.0, rotation=60.0, theme=SvgTheme::Light))]
771 fn butterfly_svg(&self, width: f64, height: f64, rotation: f64, theme: SvgTheme) -> String {
772 let diagram = CoreButterflyDiagram::from_eulumdat(&self.inner, width, height, rotation);
773 diagram.to_svg(width, height, &theme.to_core())
774 }
775
776 #[pyo3(signature = (width=600.0, height=400.0, max_curves=8, theme=SvgTheme::Light))]
778 fn cartesian_svg(&self, width: f64, height: f64, max_curves: usize, theme: SvgTheme) -> String {
779 let diagram = CoreCartesianDiagram::from_eulumdat(&self.inner, width, height, max_curves);
780 diagram.to_svg(width, height, &theme.to_core())
781 }
782
783 #[pyo3(signature = (width=700.0, height=500.0, theme=SvgTheme::Light))]
785 fn heatmap_svg(&self, width: f64, height: f64, theme: SvgTheme) -> String {
786 let diagram = CoreHeatmapDiagram::from_eulumdat(&self.inner, width, height);
787 diagram.to_svg(width, height, &theme.to_core())
788 }
789
790 #[pyo3(signature = (width=400.0, height=350.0, theme=SvgTheme::Light))]
792 fn bug_svg(&self, width: f64, height: f64, theme: SvgTheme) -> String {
793 let diagram = CoreBugDiagram::from_eulumdat(&self.inner);
794 diagram.to_svg(width, height, &theme.to_core())
795 }
796
797 #[pyo3(signature = (width=510.0, height=315.0, theme=SvgTheme::Light))]
799 fn lcs_svg(&self, width: f64, height: f64, theme: SvgTheme) -> String {
800 let diagram = CoreBugDiagram::from_eulumdat(&self.inner);
801 diagram.to_lcs_svg(width, height, &theme.to_core())
802 }
803
804 fn bug_rating(&self) -> BugRating {
806 let diagram = CoreBugDiagram::from_eulumdat(&self.inner);
807 BugRating {
808 b: diagram.rating.b,
809 u: diagram.rating.u,
810 g: diagram.rating.g,
811 }
812 }
813
814 fn zone_lumens(&self) -> ZoneLumens {
816 let diagram = CoreBugDiagram::from_eulumdat(&self.inner);
817 ZoneLumens {
818 bl: diagram.zones.bl,
819 bm: diagram.zones.bm,
820 bh: diagram.zones.bh,
821 bvh: diagram.zones.bvh,
822 fl: diagram.zones.fl,
823 fm: diagram.zones.fm,
824 fh: diagram.zones.fh,
825 fvh: diagram.zones.fvh,
826 ul: diagram.zones.ul,
827 uh: diagram.zones.uh,
828 }
829 }
830
831 #[pyo3(signature = (width=600.0, height=400.0, theme=SvgTheme::Light))]
833 fn bug_svg_with_details(&self, width: f64, height: f64, theme: SvgTheme) -> String {
834 let diagram = CoreBugDiagram::from_eulumdat(&self.inner);
835 diagram.to_svg_with_details(width, height, &theme.to_core())
836 }
837
838 fn photometric_summary(&self) -> PhotometricSummary {
845 PhotometricCalcs::photometric_summary(&self.inner)
846 }
847
848 fn gldf_data(&self) -> GldfPhotometricData {
852 PhotometricCalcs::gldf_data(&self.inner)
853 }
854
855 fn cie_flux_codes(&self) -> CieFluxCodes {
860 PhotometricCalcs::cie_flux_codes(&self.inner)
861 }
862
863 fn beam_angle(&self) -> f64 {
868 PhotometricCalcs::beam_angle(&self.inner)
869 }
870
871 fn field_angle(&self) -> f64 {
876 PhotometricCalcs::field_angle(&self.inner)
877 }
878
879 fn spacing_criteria(&self) -> (f64, f64) {
884 PhotometricCalcs::spacing_criteria(&self.inner)
885 }
886
887 fn zonal_lumens_30(&self) -> ZonalLumens30 {
892 PhotometricCalcs::zonal_lumens_30(&self.inner)
893 }
894
895 fn downward_flux(&self, arc: f64) -> f64 {
903 PhotometricCalcs::downward_flux(&self.inner, arc)
904 }
905
906 fn cut_off_angle(&self) -> f64 {
911 PhotometricCalcs::cut_off_angle(&self.inner)
912 }
913
914 fn photometric_code(&self) -> String {
924 PhotometricCalcs::photometric_code(&self.inner)
925 }
926
927 fn luminaire_efficacy_lor(&self) -> f64 {
934 PhotometricCalcs::luminaire_efficacy(&self.inner)
935 }
936
937 fn calculate_ugr(&self, params: &UgrParams) -> f64 {
945 PhotometricCalcs::ugr(&self.inner, params)
946 }
947
948 fn __repr__(&self) -> String {
949 format!(
950 "Eulumdat(name='{}', symmetry={:?}, c_planes={}, g_angles={})",
951 self.inner.luminaire_name,
952 self.inner.symmetry,
953 self.inner.c_angles.len(),
954 self.inner.g_angles.len()
955 )
956 }
957}