1use spectral_io::{
58 BatchMetadata, MeasurementType, Provenance, SpectralData, SpectrumFile, SpectrumMetadata,
59 SpectrumRecord, WavelengthAxis, WavelengthRange,
60};
61use std::{
62 env, fs,
63 path::{Path, PathBuf},
64 process,
65};
66
67struct Dataset {
72 csv_file: &'static str,
73 subdir: &'static str,
74 json_file: &'static str,
75 title: &'static str,
76 mtype: &'static str,
77 date: &'static str,
78 source: &'static str,
79 doi: &'static str,
80 columns: Vec<String>,
81}
82
83fn datasets() -> Vec<Dataset> {
84 let fl_cols: Vec<String> = (1u8..=12)
85 .map(|i| format!("FL{i}"))
86 .chain((1u8..=15).map(|i| format!("FL3.{i}")))
87 .collect();
88 let led_cols: Vec<String> = [
89 "LED-B1", "LED-B2", "LED-B3", "LED-B4", "LED-B5", "LED-BH1", "LED-RGB1", "LED-V1", "LED-V2",
90 ]
91 .iter()
92 .map(|s| s.to_string())
93 .collect();
94 let cri_cols: Vec<String> = (1u8..=14).map(|i| format!("TCS{i:02}")).collect();
95 let cfi_cols: Vec<String> = (1u8..=99).map(|i| format!("CS{i:02}")).collect();
98 let cqs_cols: Vec<String> = (1u8..=15).map(|i| format!("VS{i}")).collect();
99
100 vec![
101 Dataset {
103 csv_file: "CIE_std_illum_A_1nm.csv",
104 subdir: "illuminants",
105 json_file: "cie_std_illum_a.json",
106 title: "CIE Standard Illuminant A",
107 mtype: "irradiance",
108 date: "2018-01-01",
109 source: "CIE 015:2018 Colorimetry, 4th Edition, Equation 4.1",
110 doi: "10.25039/CIE.DS.8jsxjrsn",
111 columns: vec!["A".into()],
112 },
113 Dataset {
114 csv_file: "CIE_std_illum_D50.csv",
115 subdir: "illuminants",
116 json_file: "cie_std_illum_d50.json",
117 title: "CIE Standard Illuminant D50",
118 mtype: "irradiance",
119 date: "2022-01-01",
120 source: "ISO/CIE 11664-2:2022 Colorimetry — Part 2: CIE Standard Illuminants, Table B.1",
121 doi: "10.25039/CIE.DS.etgmuqt5",
122 columns: vec!["D50".into()],
123 },
124 Dataset {
125 csv_file: "CIE_std_illum_D65.csv",
126 subdir: "illuminants",
127 json_file: "cie_std_illum_d65.json",
128 title: "CIE Standard Illuminant D65",
129 mtype: "irradiance",
130 date: "2022-01-01",
131 source: "ISO/CIE 11664-2:2022 Colorimetry — Part 2: CIE Standard Illuminants, Table B.1",
132 doi: "10.25039/CIE.DS.hjfjmt59",
133 columns: vec!["D65".into()],
134 },
135 Dataset {
136 csv_file: "CIE_illum_C.csv",
137 subdir: "illuminants",
138 json_file: "cie_std_illum_c.json",
139 title: "CIE Standard Illuminant C",
140 mtype: "irradiance",
141 date: "2018-01-01",
142 source: "CIE 015:2018 Colorimetry, 4th Edition, Table 5",
143 doi: "10.25039/CIE.DS.mjdd2enu",
144 columns: vec!["C".into()],
145 },
146 Dataset {
147 csv_file: "CIE_illum_D55.csv",
148 subdir: "illuminants",
149 json_file: "cie_std_illum_d55.json",
150 title: "CIE Standard Illuminant D55",
151 mtype: "irradiance",
152 date: "2018-01-01",
153 source: "CIE 015:2018 Colorimetry, 4th Edition, Table 5",
154 doi: "10.25039/CIE.DS.qewfb3kp",
155 columns: vec!["D55".into()],
156 },
157 Dataset {
158 csv_file: "CIE_illum_D75.csv",
159 subdir: "illuminants",
160 json_file: "cie_std_illum_d75.json",
161 title: "CIE Standard Illuminant D75",
162 mtype: "irradiance",
163 date: "2018-01-01",
164 source: "CIE 015:2018 Colorimetry, 4th Edition, Table 5",
165 doi: "10.25039/CIE.DS.9fvcmrk4",
166 columns: vec!["D75".into()],
167 },
168 Dataset {
170 csv_file: "CIE_illum_HPs.csv",
171 subdir: "illuminants",
172 json_file: "cie_illum_hp_lamps.json",
173 title: "CIE High-Pressure Discharge Lamp Illuminants HP1–HP5",
174 mtype: "irradiance",
175 date: "2018-01-01",
176 source: "CIE 015:2018 Colorimetry, 4th Edition, Table 11",
177 doi: "10.25039/CIE.DS.f6rvvnev",
178 columns: vec!["HP1".into(), "HP2".into(), "HP3".into(), "HP4".into(), "HP5".into()],
179 },
180 Dataset {
182 csv_file: "CIE_illum_FLs.csv",
183 subdir: "illuminants",
184 json_file: "cie_illum_fl_lamps_5nm.json",
185 title: "CIE Fluorescent Lamp Illuminants FL1–FL12 and FL3.1–FL3.15 (5 nm)",
186 mtype: "irradiance",
187 date: "2018-01-01",
188 source: "CIE 015:2018 Colorimetry, 4th Edition, Tables 10.1–10.3",
189 doi: "10.25039/CIE.DS.vgssnyfg",
190 columns: fl_cols.clone(),
191 },
192 Dataset {
193 csv_file: "CIE_illum_FLs_1nm.csv",
194 subdir: "illuminants",
195 json_file: "cie_illum_fl_lamps_1nm.json",
196 title: "CIE Fluorescent Lamp Illuminants FL1–FL12 and FL3.1–FL3.15 (1 nm)",
197 mtype: "irradiance",
198 date: "2018-01-01",
199 source: "CIE 015:2018 Colorimetry, 4th Edition, Tables 10.1–10.3",
200 doi: "10.25039/CIE.DS.54hy6srn",
201 columns: fl_cols,
202 },
203 Dataset {
205 csv_file: "CIE_illum_LEDs.csv",
206 subdir: "illuminants",
207 json_file: "cie_illum_led_lamps_5nm.json",
208 title: "CIE LED Illuminants LED-B1–B5, LED-BH1, LED-RGB1, LED-V1–V2 (5 nm)",
209 mtype: "irradiance",
210 date: "2018-01-01",
211 source: "CIE 015:2018 Colorimetry, 4th Edition, Table 12",
212 doi: "10.25039/CIE.DS.vgssnyfg",
213 columns: led_cols.clone(),
214 },
215 Dataset {
216 csv_file: "CIE_illum_LEDs_1nm.csv",
217 subdir: "illuminants",
218 json_file: "cie_illum_led_lamps_1nm.json",
219 title: "CIE LED Illuminants LED-B1–B5, LED-BH1, LED-RGB1, LED-V1–V2 (1 nm)",
220 mtype: "irradiance",
221 date: "2018-01-01",
222 source: "CIE 015:2018 Colorimetry, 4th Edition, Table 12",
223 doi: "10.25039/CIE.DS.dhcw57sd",
224 columns: led_cols,
225 },
226 Dataset {
228 csv_file: "CIE_illum_ID50.csv",
229 subdir: "illuminants",
230 json_file: "cie_illum_id50.json",
231 title: "CIE Indoor Daylight Illuminant ID50",
232 mtype: "irradiance",
233 date: "2009-01-01",
234 source: "CIE 184:2009 Indoor Daylight Illuminants",
235 doi: "10.25039/CIE.DS.r4gcnrzc",
236 columns: vec!["ID50".into()],
237 },
238 Dataset {
239 csv_file: "CIE_illum_ID65.csv",
240 subdir: "illuminants",
241 json_file: "cie_illum_id65.json",
242 title: "CIE Indoor Daylight Illuminant ID65",
243 mtype: "irradiance",
244 date: "2009-01-01",
245 source: "CIE 184:2009 Indoor Daylight Illuminants",
246 doi: "10.25039/CIE.DS.bd53qdqk",
247 columns: vec!["ID65".into()],
248 },
249 Dataset {
251 csv_file: "CIE_illum_Dxx_comp.csv",
252 subdir: "illuminants",
253 json_file: "cie_illum_daylight_components.json",
254 title: "CIE Daylight Spectral Components S0, S1, S2",
255 mtype: "irradiance",
256 date: "2018-01-01",
257 source: "CIE 015:2018 Colorimetry, 4th Edition",
258 doi: "10.25039/CIE.DS.w7zunnny",
259 columns: vec!["S0".into(), "S1".into(), "S2".into()],
260 },
261 Dataset {
263 csv_file: "CIE_srf_cri.csv",
264 subdir: "color_rendering",
265 json_file: "cie_cri_14_test_samples.json",
266 title: "CIE Colour Rendering Index — Spectral Radiance Factors of 14 Test Colour Samples",
267 mtype: "reflectance",
268 date: "1995-01-01",
269 source: "CIE 13.3:1995 Method of Measuring and Specifying Colour Rendering Properties of Light Sources, Table A7.1",
270 doi: "10.25039/CIE.DS.wuiuu9cz",
271 columns: cri_cols,
272 },
273 Dataset {
274 csv_file: "CIE_srf_cfi.csv",
275 subdir: "color_rendering",
276 json_file: "cie_cfi_99_test_samples_5nm.json",
277 title: "CIE Colour Fidelity Index — Spectral Radiance Factors of 99 Test Colour Samples (5 nm)",
278 mtype: "reflectance",
279 date: "2017-01-01",
280 source: "CIE 224:2017 Colour Fidelity Index for Accurate Scientific Use, Table A.1",
281 doi: "10.25039/CIE.DS.wi5idbqu",
282 columns: cfi_cols.clone(),
283 },
284 Dataset {
285 csv_file: "CIE_srf_cfi_1nm.csv",
286 subdir: "color_rendering",
287 json_file: "cie_cfi_99_test_samples_1nm.json",
288 title: "CIE Colour Fidelity Index — Spectral Radiance Factors of 99 Test Colour Samples (1 nm)",
289 mtype: "reflectance",
290 date: "2017-01-01",
291 source: "CIE 224:2017 Colour Fidelity Index for Accurate Scientific Use, CFI Calculator Toolbox",
292 doi: "10.25039/CIE.DS.8svs5rqd",
293 columns: cfi_cols,
294 },
295 Dataset {
296 csv_file: "CIE_srf_CQS_5nm.csv",
297 subdir: "color_rendering",
298 json_file: "cie_cqs_15_test_samples.json",
299 title: "CIE Colour Quality Scale — Spectral Radiance Factors of 15 Test Colour Samples (5 nm)",
300 mtype: "reflectance",
301 date: "2024-01-01",
302 source: "CIE 253:2024 Colour Quality Scale, Table A.1",
303 doi: "10.25039/CIE.DS.yzfhz3cm",
304 columns: cqs_cols,
305 },
306 Dataset {
307 csv_file: "CIE_srf_FCI_5nm.csv",
308 subdir: "color_rendering",
309 json_file: "cie_four_colour_rygb.json",
310 title: "CIE Four-Colour Combination — Spectral Radiance Factors of Red, Yellow, Green, Blue (5 nm)",
311 mtype: "reflectance",
312 date: "2018-01-01",
313 source: "CIE 015:2018 Colorimetry, 4th Edition",
314 doi: "10.25039/CIE.DS.vkss79ef",
315 columns: vec!["Red".into(), "Yellow".into(), "Green".into(), "Blue".into()],
316 },
317 Dataset {
318 csv_file: "CIE_srf_PS_5nm.csv",
319 subdir: "color_rendering",
320 json_file: "cie_japanese_skin_complexion.json",
321 title: "CIE Test Colour Sample 15 — Japanese Skin Complexion (5 nm)",
322 mtype: "reflectance",
323 date: "2018-01-01",
324 source: "CIE 015:2018 Colorimetry, 4th Edition",
325 doi: "10.25039/CIE.DS.7chm7z5h",
326 columns: vec!["beta_15".into()],
327 },
328 Dataset {
330 csv_file: "CIE_xyz_1931_2deg.csv",
331 subdir: "sensitivity",
332 json_file: "cie_cmf_1931_2deg.json",
333 title: "CIE 1931 Colour-Matching Functions, 2° Observer",
334 mtype: "sensitivity",
335 date: "2019-01-01",
336 source: "ISO/CIE 11664-1:2019 Colorimetry — Part 1: CIE Standard Colorimetric Observers, Annex A",
337 doi: "10.25039/CIE.DS.xvudnb9b",
338 columns: vec!["x_bar".into(), "y_bar".into(), "z_bar".into()],
339 },
340 Dataset {
341 csv_file: "CIE_xyz_1964_10deg.csv",
342 subdir: "sensitivity",
343 json_file: "cie_cmf_1964_10deg.json",
344 title: "CIE 1964 Colour-Matching Functions, 10° Observer",
345 mtype: "sensitivity",
346 date: "2019-01-01",
347 source: "ISO/CIE 11664-1:2019 Colorimetry — Part 1: CIE Standard Colorimetric Observers, Annex B",
348 doi: "10.25039/CIE.DS.sqksu2n5",
349 columns: vec!["x_bar_10".into(), "y_bar_10".into(), "z_bar_10".into()],
350 },
351 Dataset {
353 csv_file: "CIE_lms_cf_2deg.csv",
354 subdir: "sensitivity",
355 json_file: "cie_lms_cone_fundamentals_2deg.json",
356 title: "CIE 2006 LMS Cone Fundamentals, 2° Field Size",
357 mtype: "sensitivity",
358 date: "2006-01-01",
359 source: "CIE 170-1:2006 Fundamental Chromaticity Diagram with Physiological Axes — Part 1, Table 2",
360 doi: "10.25039/CIE.DS.tijidesg",
361 columns: vec!["l_bar".into(), "m_bar".into(), "s_bar".into()],
362 },
363 Dataset {
364 csv_file: "CIE_lms_cf_10deg.csv",
365 subdir: "sensitivity",
366 json_file: "cie_lms_cone_fundamentals_10deg.json",
367 title: "CIE 2006 LMS Cone Fundamentals, 10° Field Size",
368 mtype: "sensitivity",
369 date: "2006-01-01",
370 source: "CIE 170-1:2006 Fundamental Chromaticity Diagram with Physiological Axes — Part 1, Table 3",
371 doi: "10.25039/CIE.DS.nxsqeri8",
372 columns: vec!["l_bar_10".into(), "m_bar_10".into(), "s_bar_10".into()],
373 },
374 Dataset {
378 csv_file: "CIE_cfb_stv_2deg.csv",
379 subdir: "sensitivity",
380 json_file: "cie_cfb_xyz_2deg.json",
381 title: "CIE Cone-Fundamental-Based Spectral Tristimulus Values, 2° Field Size",
382 mtype: "sensitivity",
383 date: "2015-01-01",
384 source: "CIE 170-2:2015 Fundamental Chromaticity Diagram with Physiological Axes — Part 2, Table 1",
385 doi: "10.25039/CIE.DS.548rw69q",
386 columns: vec!["x_F".into(), "y_F".into(), "z_F".into()],
387 },
388 Dataset {
389 csv_file: "CIE_cfb_stv_10deg.csv",
390 subdir: "sensitivity",
391 json_file: "cie_cfb_xyz_10deg.json",
392 title: "CIE Cone-Fundamental-Based Spectral Tristimulus Values, 10° Field Size",
393 mtype: "sensitivity",
394 date: "2015-01-01",
395 source: "CIE 170-2:2015 Fundamental Chromaticity Diagram with Physiological Axes — Part 2, Table 3",
396 doi: "10.25039/CIE.DS.dm6qiig7",
397 columns: vec!["x_F10".into(), "y_F10".into(), "z_F10".into()],
398 },
399 Dataset {
400 csv_file: "CIE_cfb_sle_2deg.csv",
401 subdir: "sensitivity",
402 json_file: "cie_cfb_luminous_efficiency_2deg.json",
403 title: "CIE Cone-Fundamental-Based Spectral Luminous Efficiency, 2° Field Size",
404 mtype: "sensitivity",
405 date: "2015-01-01",
406 source: "CIE 170-2:2015 Fundamental Chromaticity Diagram with Physiological Axes — Part 2, Table 2",
407 doi: "10.25039/CIE.DS.pyqkh5rw",
408 columns: vec!["V_F".into()],
409 },
410 Dataset {
411 csv_file: "CIE_cfb_sle_10deg.csv",
412 subdir: "sensitivity",
413 json_file: "cie_cfb_luminous_efficiency_10deg.json",
414 title: "CIE Cone-Fundamental-Based Spectral Luminous Efficiency, 10° Field Size",
415 mtype: "sensitivity",
416 date: "2015-01-01",
417 source: "CIE 170-2:2015 Fundamental Chromaticity Diagram with Physiological Axes — Part 2, Table 4",
418 doi: "10.25039/CIE.DS.8mrru44q",
419 columns: vec!["V_F10".into()],
420 },
421 Dataset {
423 csv_file: "CIE_sle_photopic.csv",
424 subdir: "sensitivity",
425 json_file: "cie_luminous_efficiency_photopic.json",
426 title: "CIE Spectral Luminous Efficiency for Photopic Vision V(λ)",
427 mtype: "sensitivity",
428 date: "2019-01-01",
429 source: "ISO/CIE 11664-1:2019 Colorimetry — Part 1: CIE Standard Colorimetric Observers, Table 1",
430 doi: "10.25039/CIE.DS.dktna2s3",
431 columns: vec!["V".into()],
432 },
433 Dataset {
434 csv_file: "CIE_sle_scotopic.csv",
435 subdir: "sensitivity",
436 json_file: "cie_luminous_efficiency_scotopic.json",
437 title: "CIE Spectral Luminous Efficiency for Scotopic Vision V′(λ)",
438 mtype: "sensitivity",
439 date: "2019-01-01",
440 source: "ISO/CIE 11664-1:2019 Colorimetry — Part 1: CIE Standard Colorimetric Observers, Table 2",
441 doi: "10.25039/CIE.DS.gr6w4b5g",
442 columns: vec!["V_prime".into()],
443 },
444 Dataset {
446 csv_file: "CIE_RefSpectrum_L41.csv",
447 subdir: "illuminants",
448 json_file: "cie_reference_spectrum_l41.json",
449 title: "CIE Reference Spectrum L41",
450 mtype: "irradiance",
451 date: "2023-01-01",
452 source: "CIE 251:2023 CIE L41 Spectral Power Distribution, Table 1",
453 doi: "10.25039/CIE.DS.van56dfj",
454 columns: vec!["L41".into()],
455 },
456 Dataset {
458 csv_file: "CIE_1st_deriv_meta_ind.csv",
459 subdir: "sensitivity",
460 json_file: "cie_metamerism_index_1st_deviation.json",
461 title: "CIE First Deviation Function for Special Metamerism Index (Change in Observer)",
462 mtype: "sensitivity",
463 date: "2018-01-01",
464 source: "CIE 015:2018 Colorimetry, 4th Edition, Table A.4",
465 doi: "10.25039/CIE.DS.caky9gj2",
466 columns: vec!["f1".into(), "f2".into(), "f3".into()],
467 },
468 ]
469}
470
471fn convert_alpha_opic(input_dir: &Path, output_dir: &Path) -> bool {
481 let csv_path = input_dir.join("CIE_a-opic_action_spectra.csv");
482 if !csv_path.exists() {
483 eprintln!(" SKIP CIE_a-opic_action_spectra.csv (file not found)");
484 return false;
485 }
486 let raw = match fs::read_to_string(&csv_path) {
487 Ok(s) => s,
488 Err(e) => {
489 eprintln!(" ERROR reading CIE_a-opic_action_spectra.csv: {e}");
490 return false;
491 }
492 };
493
494 let ids = ["s_sc", "s_mc", "s_lc", "s_rh", "s_mel"];
495 let mut col_points: [Vec<(f64, f64)>; 5] = Default::default();
496
497 for line in raw.lines() {
498 let line = line.trim_end_matches('\r');
499 let fields: Vec<&str> = line.split(',').collect();
500 if fields.len() < 6 {
501 continue;
502 }
503 let Ok(wl) = fields[0]
504 .trim()
505 .trim_start_matches('\u{FEFF}')
506 .parse::<f64>()
507 else {
508 continue;
509 };
510 for (i, col) in col_points.iter_mut().enumerate() {
511 let v_str = fields[i + 1].trim();
512 if v_str.eq_ignore_ascii_case("NaN") {
513 continue;
514 }
515 if let Ok(v) = v_str.parse::<f64>() {
516 col.push((wl, v));
517 }
518 }
519 }
520
521 let copyright = "© International Commission on Illumination (CIE). \
522 Licensed CC BY-SA 4.0. https://cie.co.at/data-tables";
523 let notes = "Source: CIE S026/E:2018 CIE System for Metrology of Optical Radiation \
524 for ipRGC-Influenced Responses to Light, Table 2. \
525 DOI: https://doi.org/10.25039/CIE.DS.vqqhzp5a";
526
527 let spectra: Vec<SpectrumRecord> = ids
528 .iter()
529 .zip(col_points.iter())
530 .map(|(id, pts)| {
531 let wavelengths: Vec<f64> = pts.iter().map(|(w, _)| *w).collect();
532 let values: Vec<f64> = pts.iter().map(|(_, v)| *v).collect();
533 let wavelength_axis = if wavelengths.len() >= 2 {
534 let start = wavelengths[0];
535 let end = *wavelengths.last().unwrap();
536 let interval = wavelengths[1] - wavelengths[0];
537 let uniform = wavelengths
538 .windows(2)
539 .all(|w| (w[1] - w[0] - interval).abs() < 1e-6);
540 if uniform {
541 WavelengthAxis {
542 values_nm: None,
543 range_nm: Some(WavelengthRange {
544 start,
545 end,
546 interval,
547 }),
548 }
549 } else {
550 WavelengthAxis {
551 values_nm: Some(wavelengths),
552 range_nm: None,
553 }
554 }
555 } else {
556 WavelengthAxis {
557 values_nm: Some(wavelengths),
558 range_nm: None,
559 }
560 };
561 SpectrumRecord {
562 id: id.to_string(),
563 metadata: SpectrumMetadata {
564 measurement_type: MeasurementType::Sensitivity,
565 date: "2018-01-01".to_string(),
566 title: Some(
567 "CIE Alpha-Opic Action Spectra \
568 (S-cone, M-cone, L-cone, Rod, Melanopsin)"
569 .to_string(),
570 ),
571 copyright: Some(copyright.to_string()),
572 description: None,
573 sample_id: None,
574 time: None,
575 operator: None,
576 instrument: None,
577 measurement_conditions: None,
578 surface: None,
579 sample_backing: None,
580 tags: None,
581 custom: None,
582 },
583 wavelength_axis,
584 spectral_data: SpectralData {
585 values,
586 uncertainty: None,
587 scale: None,
588 },
589 provenance: Some(Provenance {
590 source_file: Some(
591 "https://files.cie.co.at/CIE_a-opic_action_spectra.csv".to_string(),
592 ),
593 source_format: Some("CIE CSV".to_string()),
594 notes: Some(notes.to_string()),
595 software: None,
596 software_version: None,
597 processing_steps: None,
598 }),
599 color_science: None,
600 }
601 })
602 .collect();
603
604 let file = SpectrumFile::Batch {
605 schema_version: "1.0.0".to_string(),
606 batch_metadata: Some(Box::new(BatchMetadata {
607 title: Some(
608 "CIE Alpha-Opic Action Spectra (S-cone, M-cone, L-cone, Rod, Melanopsin)"
609 .to_string(),
610 ),
611 description: None,
612 operator: None,
613 date: None,
614 instrument: None,
615 measurement_conditions: None,
616 })),
617 spectra,
618 };
619
620 let out_dir = output_dir.join("sensitivity");
621 if let Err(e) = fs::create_dir_all(&out_dir) {
622 eprintln!(" ERROR creating {}: {e}", out_dir.display());
623 return false;
624 }
625 let out_path = out_dir.join("cie_alpha_opic_action_spectra.json");
626 let json = match serde_json::to_string_pretty(&file) {
627 Ok(j) => j,
628 Err(e) => {
629 eprintln!(" ERROR serialising cie_alpha_opic_action_spectra.json: {e}");
630 return false;
631 }
632 };
633 match fs::write(&out_path, json) {
634 Ok(()) => {
635 eprintln!(
636 " OK CIE_a-opic_action_spectra.csv → {} (5 spectra)",
637 out_path.display()
638 );
639 true
640 }
641 Err(e) => {
642 eprintln!(" ERROR writing {}: {e}", out_path.display());
643 false
644 }
645 }
646}
647
648fn strip_nan_entries(file: &mut SpectrumFile) {
657 let strip = |sp: &mut SpectrumRecord| {
658 if !sp.spectral_data.values.iter().any(|v: &f64| v.is_nan()) {
659 return;
660 }
661 let wls = sp.wavelength_axis.wavelengths_nm();
662 let pairs: Vec<(f64, f64)> = wls
663 .iter()
664 .cloned()
665 .zip(sp.spectral_data.values.iter().cloned())
666 .filter(|(_, v)| !v.is_nan())
667 .collect();
668 if pairs.len() < 2 {
669 return;
670 }
671 let wavelengths: Vec<f64> = pairs.iter().map(|(w, _)| *w).collect();
672 let values: Vec<f64> = pairs.iter().map(|(_, v)| *v).collect();
673 let interval = wavelengths[1] - wavelengths[0];
674 let uniform = wavelengths
675 .windows(2)
676 .all(|w| (w[1] - w[0] - interval).abs() < 1e-6);
677 sp.wavelength_axis = if uniform {
678 WavelengthAxis {
679 range_nm: Some(WavelengthRange {
680 start: wavelengths[0],
681 end: *wavelengths.last().unwrap(),
682 interval,
683 }),
684 values_nm: None,
685 }
686 } else {
687 WavelengthAxis {
688 values_nm: Some(wavelengths),
689 range_nm: None,
690 }
691 };
692 sp.spectral_data.values = values;
693 };
694 match file {
695 SpectrumFile::Single { spectrum, .. } => strip(spectrum),
696 SpectrumFile::Batch { spectra, .. } => {
697 for sp in spectra.iter_mut() {
698 strip(sp);
699 }
700 }
701 }
702}
703
704fn count_data_cols(csv_content: &str) -> usize {
708 for line in csv_content.lines() {
709 let t = line.trim();
710 if t.is_empty() || t.starts_with('#') {
711 continue;
712 }
713 if t.split(',').next().unwrap_or("").parse::<f64>().is_ok() {
714 return t.split(',').count().saturating_sub(1);
715 }
716 }
717 0
718}
719
720fn build_header(ds: &Dataset, n_data_cols: usize) -> String {
722 let col_names: Vec<&str> = ds
723 .columns
724 .iter()
725 .map(String::as_str)
726 .take(n_data_cols)
727 .collect();
728 if col_names.len() < n_data_cols {
729 eprintln!(
730 " WARN {}: CSV has {n_data_cols} data columns but only {} names defined \
731 — extra columns will receive auto-generated ids",
732 ds.csv_file,
733 col_names.len()
734 );
735 }
736 let col_row = format!("wavelength_nm,{}", col_names.join(","));
737
738 format!(
739 "Title: {title}\n\
740 Measurement_Type: {mtype}\n\
741 Date: {date}\n\
742 Copyright: \u{00a9} International Commission on Illumination (CIE). \
743 Licensed CC BY-SA 4.0. https://cie.co.at/data-tables\n\
744 Notes: Source: {source}. DOI: https://doi.org/{doi}\n\
745 \n\
746 {col_row}\n",
747 title = ds.title,
748 mtype = ds.mtype,
749 date = ds.date,
750 source = ds.source,
751 doi = ds.doi,
752 col_row = col_row,
753 )
754}
755
756fn set_provenance(file: &mut SpectrumFile, csv_file: &str) {
758 let url = format!("https://files.cie.co.at/{csv_file}");
759 let fix = |prov: &mut Option<spectral_io::Provenance>| {
760 if let Some(p) = prov {
761 p.source_file = Some(url.clone());
762 p.source_format = Some("CIE CSV".into());
763 }
764 };
765 match file {
766 SpectrumFile::Single { spectrum, .. } => fix(&mut spectrum.provenance),
767 SpectrumFile::Batch { spectra, .. } => {
768 for sp in spectra.iter_mut() {
769 fix(&mut sp.provenance);
770 }
771 }
772 }
773}
774
775fn main() {
780 let args: Vec<String> = env::args().collect();
781 let mut input_dir = PathBuf::from("data/cie-raw");
782 let mut output_dir = PathBuf::from("data/spectral-io/cie");
783
784 let mut i = 1usize;
785 while i < args.len() {
786 match args[i].as_str() {
787 "--input" => {
788 i += 1;
789 if i < args.len() {
790 input_dir = PathBuf::from(&args[i]);
791 }
792 }
793 "--output" => {
794 i += 1;
795 if i < args.len() {
796 output_dir = PathBuf::from(&args[i]);
797 }
798 }
799 arg => {
800 eprintln!("Unknown argument: {arg}");
801 process::exit(1);
802 }
803 }
804 i += 1;
805 }
806
807 let datasets = datasets();
808 let mut ok = 0usize;
809 let mut skipped = 0usize;
810 let mut failed = 0usize;
811
812 for ds in &datasets {
813 let csv_path = input_dir.join(ds.csv_file);
814 if !csv_path.exists() {
815 eprintln!(" SKIP {} (file not found)", ds.csv_file);
816 skipped += 1;
817 continue;
818 }
819
820 let raw_owned = match fs::read_to_string(&csv_path) {
821 Ok(s) => s,
822 Err(e) => {
823 eprintln!(" ERROR reading {}: {e}", ds.csv_file);
824 failed += 1;
825 continue;
826 }
827 };
828 let raw = raw_owned.strip_prefix('\u{FEFF}').unwrap_or(&raw_owned);
833
834 let n_cols = count_data_cols(raw);
835 if n_cols == 0 {
836 eprintln!(" ERROR {}: no data rows found", ds.csv_file);
837 failed += 1;
838 continue;
839 }
840
841 let synthetic = format!("{}{}", build_header(ds, n_cols), raw);
842
843 let mut file = match SpectrumFile::from_csv_str(&synthetic) {
844 Ok(f) => f,
845 Err(e) => {
846 eprintln!(" ERROR parsing {}: {e}", ds.csv_file);
847 failed += 1;
848 continue;
849 }
850 };
851
852 strip_nan_entries(&mut file);
853 set_provenance(&mut file, ds.csv_file);
854
855 let out_dir = output_dir.join(ds.subdir);
856 if let Err(e) = fs::create_dir_all(&out_dir) {
857 eprintln!(" ERROR creating {}: {e}", out_dir.display());
858 failed += 1;
859 continue;
860 }
861
862 let json = match serde_json::to_string_pretty(&file) {
863 Ok(j) => j,
864 Err(e) => {
865 eprintln!(" ERROR serialising {}: {e}", ds.json_file);
866 failed += 1;
867 continue;
868 }
869 };
870
871 let out_path = out_dir.join(ds.json_file);
872 if let Err(e) = fs::write(&out_path, &json) {
873 eprintln!(" ERROR writing {}: {e}", out_path.display());
874 failed += 1;
875 continue;
876 }
877
878 let n_spectra = file.spectra().len();
879 eprintln!(
880 " OK {} → {} ({} {})",
881 ds.csv_file,
882 out_path.display(),
883 n_spectra,
884 if n_spectra == 1 {
885 "spectrum"
886 } else {
887 "spectra"
888 }
889 );
890 ok += 1;
891 }
892
893 if convert_alpha_opic(&input_dir, &output_dir) {
895 ok += 1;
896 } else if !input_dir.join("CIE_a-opic_action_spectra.csv").exists() {
897 skipped += 1;
898 } else {
899 failed += 1;
900 }
901
902 eprintln!();
903 eprintln!("{ok} converted, {skipped} skipped, {failed} failed");
904 if failed > 0 {
905 process::exit(1);
906 }
907}