@@ -455,6 +455,98 @@ def test_dump_uneven_lists(self, mock_experiment):
455455 os .unlink (filepath )
456456
457457
458+ class TestDumpAllHarmonics :
459+ """Tests for the ``save_all_harmonics`` output files of Sample.dump_reduced_data_to_csv."""
460+
461+ @staticmethod
462+ def _make_two_bank_sample (experiment : Experiment , save_all_harmonics : bool , tmp_path ) -> Sample :
463+ experiment .output_dir = str (tmp_path )
464+ experiment ._config .save_all_harmonics = save_all_harmonics
465+ sample = _make_sample (experiment , "test" , [])
466+ # One (Q,I,E) curve per detector bank, both for the unscaled and the rescaled data
467+ sample .detector_data = [
468+ IQData (q = [0.1 , 0.2 ], i = [100.0 , 200.0 ], e = [10.0 , 14.0 ]),
469+ IQData (q = [0.3 , 0.4 ], i = [300.0 , 400.0 ], e = [17.0 , 20.0 ]),
470+ ]
471+ sample .data_scaled = [
472+ IQData (q = [0.1 , 0.2 ], i = [1.0 , 2.0 ], e = [0.1 , 0.2 ]),
473+ IQData (q = [0.3 , 0.4 ], i = [3.0 , 4.0 ], e = [0.3 , 0.4 ]),
474+ ]
475+ return sample
476+
477+ def test_writes_one_file_per_harmonic (self , mock_experiment_2banks , tmp_path ):
478+ """With save_all_harmonics, each bank is written flat as ``_det_<n>``."""
479+ sample = self ._make_two_bank_sample (mock_experiment_2banks , True , tmp_path )
480+
481+ sample .dump_reduced_data_to_csv (background_subtracted_data = False )
482+
483+ for harmonic in (1 , 2 ):
484+ assert (tmp_path / f"UN_test_det_{ harmonic } _unscaled.txt" ).is_file ()
485+ assert (tmp_path / f"UN_test_det_{ harmonic } .txt" ).is_file ()
486+ assert not list (tmp_path .glob ("bank_*" )), "no per-bank subdirectories should be created"
487+
488+ def test_higher_harmonics_hold_their_own_bank_data (self , mock_experiment_2banks , tmp_path ):
489+ """The ``_det_2`` files hold bank 2's curve, not a copy of bank 1's."""
490+ sample = self ._make_two_bank_sample (mock_experiment_2banks , True , tmp_path )
491+
492+ sample .dump_reduced_data_to_csv (background_subtracted_data = False )
493+
494+ rows = list (csv .reader ((tmp_path / "UN_test_det_2_unscaled.txt" ).read_text ().splitlines ()))
495+ assert [float (row [0 ]) for row in rows ] == [0.3 , 0.4 ]
496+ assert [float (row [1 ]) for row in rows ] == [300.0 , 400.0 ]
497+
498+ def test_only_first_harmonic_without_the_flag (self , mock_experiment_2banks , tmp_path ):
499+ """Without save_all_harmonics, only the first harmonic is written."""
500+ sample = self ._make_two_bank_sample (mock_experiment_2banks , False , tmp_path )
501+
502+ sample .dump_reduced_data_to_csv (background_subtracted_data = False )
503+
504+ assert (tmp_path / "UN_test_det_1_unscaled.txt" ).is_file ()
505+ assert (tmp_path / "UN_test_det_1.txt" ).is_file ()
506+ assert not (tmp_path / "UN_test_det_2_unscaled.txt" ).exists ()
507+ assert not (tmp_path / "UN_test_det_2.txt" ).exists ()
508+
509+ def test_missing_first_detector_harmonic_aborts_output (self , mock_experiment_2banks , tmp_path ):
510+ """Missing first-harmonic detector data must abort output generation."""
511+ sample = self ._make_two_bank_sample (mock_experiment_2banks , True , tmp_path )
512+ sample .detector_data = []
513+
514+ with pytest .raises (RuntimeError , match = "first harmonic data is missing" ):
515+ sample .dump_reduced_data_to_csv (scaled_data = False , background_subtracted_data = False )
516+
517+ def test_missing_first_scaled_harmonic_aborts_output (self , mock_experiment_2banks , tmp_path ):
518+ """Missing first-harmonic scaled data must abort output generation."""
519+ sample = self ._make_two_bank_sample (mock_experiment_2banks , True , tmp_path )
520+ sample .data_scaled = []
521+
522+ with pytest .raises (RuntimeError , match = "first harmonic data is missing" ):
523+ sample .dump_reduced_data_to_csv (detector_data = False , background_subtracted_data = False )
524+
525+ def test_missing_higher_harmonics_warns_and_skips_files (self , mock_experiment_2banks , tmp_path , caplog ):
526+ """Missing higher harmonics should be warned about and skipped."""
527+ sample = self ._make_two_bank_sample (mock_experiment_2banks , True , tmp_path )
528+ sample .detector_data = sample .detector_data [:1 ]
529+ sample .data_scaled = sample .data_scaled [:1 ]
530+
531+ with caplog .at_level (logging .WARNING ):
532+ sample .dump_reduced_data_to_csv (background_subtracted_data = False )
533+
534+ assert (tmp_path / "UN_test_det_1_unscaled.txt" ).is_file ()
535+ assert (tmp_path / "UN_test_det_1.txt" ).is_file ()
536+ assert not (tmp_path / "UN_test_det_2_unscaled.txt" ).exists ()
537+ assert not (tmp_path / "UN_test_det_2.txt" ).exists ()
538+ assert "No detector data is available" in caplog .text
539+ assert "No scaled data is available" in caplog .text
540+
541+ def test_empty_first_scaled_harmonic_aborts_output (self , mock_experiment_2banks , tmp_path ):
542+ """An empty first-harmonic scaled curve must abort output generation."""
543+ sample = self ._make_two_bank_sample (mock_experiment_2banks , True , tmp_path )
544+ sample .data_scaled [0 ] = IQData ()
545+
546+ with pytest .raises (RuntimeError , match = "first harmonic data is missing" ):
547+ sample .dump_reduced_data_to_csv (detector_data = False , background_subtracted_data = False )
548+
549+
458550class TestDumpBackgroundSubtracted :
459551 """Tests for dumping the background-subtracted data file."""
460552
0 commit comments