From ec61bab1359600cb53dcd8d90f29c02170a602e3 Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:38:16 +0530 Subject: [PATCH 01/24] fix: compute Experiment start/end time as union across all devices --- experanto/experiment.py | 4 +- tests/test_experiment.py | 163 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 tests/test_experiment.py diff --git a/experanto/experiment.py b/experanto/experiment.py index faa2fed1..22ea9a5d 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -121,8 +121,8 @@ def _load_devices(self) -> None: ) self.devices[d.name] = dev - self.start_time = dev.start_time - self.end_time = dev.end_time + self.start_time = min(self.start_time, dev.start_time) + self.end_time = max(self.end_time, dev.end_time) logger.info("Parsing finished") @property diff --git a/tests/test_experiment.py b/tests/test_experiment.py new file mode 100644 index 00000000..e7e846fc --- /dev/null +++ b/tests/test_experiment.py @@ -0,0 +1,163 @@ +import shutil +from contextlib import contextmanager +from pathlib import Path + +import numpy as np +import pytest +import yaml + +from experanto.experiment import Experiment + +EXPERIMENT_ROOT = Path("tests/experiment_data") + + +@contextmanager +def create_two_device_experiment( + device0_start=0.0, + device0_end=10.0, + device1_start=1.0, + device1_end=8.0, + sampling_rate=10.0, +): + """Create a temporary experiment with two sequence devices with different time ranges.""" + try: + for device_name, start, end in [ + ("device_0", device0_start, device0_end), + ("device_1", device1_start, device1_end), + ]: + device_root = EXPERIMENT_ROOT / device_name + (device_root / "meta").mkdir(parents=True, exist_ok=True) + + n_samples = int((end - start) * sampling_rate) + 1 + timestamps = np.linspace(start, end, n_samples) + data = np.random.rand(n_samples, 5) + + np.save(device_root / "timestamps.npy", timestamps) + np.save(device_root / "data.npy", data) + + meta = { + "start_time": start, + "end_time": end, + "modality": "sequence", + "sampling_rate": sampling_rate, + "phase_shift_per_signal": False, + "is_mem_mapped": False, + "n_signals": 5, + "n_timestamps": n_samples, + "dtype": "float64", + } + with open(device_root / "meta.yml", "w") as f: + yaml.safe_dump(meta, f) + + yield EXPERIMENT_ROOT + + finally: + shutil.rmtree(EXPERIMENT_ROOT) + + +@contextmanager +def create_three_device_experiment(): + """Create a temporary experiment with three sequence devices with different time ranges.""" + try: + for device_name, start, end in [ + ("device_0", 0.0, 10.0), + ("device_1", 1.0, 8.0), + ("device_2", 2.0, 9.0), + ]: + device_root = EXPERIMENT_ROOT / device_name + (device_root / "meta").mkdir(parents=True, exist_ok=True) + + sampling_rate = 10.0 + n_samples = int((end - start) * sampling_rate) + 1 + timestamps = np.linspace(start, end, n_samples) + data = np.random.rand(n_samples, 5) + + np.save(device_root / "timestamps.npy", timestamps) + np.save(device_root / "data.npy", data) + + meta = { + "start_time": start, + "end_time": end, + "modality": "sequence", + "sampling_rate": sampling_rate, + "phase_shift_per_signal": False, + "is_mem_mapped": False, + "n_signals": 5, + "n_timestamps": n_samples, + "dtype": "float64", + } + with open(device_root / "meta.yml", "w") as f: + yaml.safe_dump(meta, f) + + yield EXPERIMENT_ROOT + + finally: + shutil.rmtree(EXPERIMENT_ROOT) + + +def get_two_device_config(): + return { + "device_0": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}, + "device_1": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}, + } + + +def test_experiment_start_end_time_reflects_union(): + """ + Experiment.start_time and end_time should reflect the union + of all device time ranges — the earliest start and latest end + across all devices, not just the last loaded device. + """ + with create_two_device_experiment( + device0_start=1.0, + device0_end=8.0, + device1_start=0.0, + device1_end=10.0, + ) as experiment_path: + experiment = Experiment( + root_folder=experiment_path, + modality_config=get_two_device_config(), + ) + + # Union: start = min(1.0, 0.0) = 0.0, end = max(8.0, 10.0) = 10.0 + assert experiment.start_time == pytest.approx( + 0.0 + ), f"Expected start_time=0.0, got {experiment.start_time}" + assert experiment.end_time == pytest.approx( + 10.0 + ), f"Expected end_time=10.0, got {experiment.end_time}" + + +def test_experiment_single_device_time_range(): + """With a single device, start_time and end_time should match that device's range.""" + with create_two_device_experiment( + device0_start=2.0, + device0_end=9.0, + ) as experiment_path: + config = {"device_0": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}} + experiment = Experiment( + root_folder=experiment_path, + modality_config=config, + ) + + assert experiment.start_time == pytest.approx(2.0) + assert experiment.end_time == pytest.approx(9.0) + + +def test_experiment_start_end_time_three_devices(): + """With three devices, start_time and end_time should reflect the union of all three — + earliest start and latest end across all devices.""" + with create_three_device_experiment() as experiment_path: + config = { + "device_0": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}, + "device_1": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}, + "device_2": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}, + } + experiment = Experiment( + root_folder=experiment_path, + modality_config=config, + ) + + # Union: start = min(0.0, 1.0, 2.0) = 0.0, end = max(10.0, 8.0, 9.0) = 10.0 + assert experiment.start_time == pytest.approx(0.0) + assert experiment.end_time == pytest.approx(10.0) From 100875884825b8c7d8c478d02edcae61fa96d082 Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:14:40 +0530 Subject: [PATCH 02/24] test: use pytest tmp_path for cleaner test isolation --- tests/test_experiment.py | 227 +++++++++++++-------------------------- 1 file changed, 77 insertions(+), 150 deletions(-) diff --git a/tests/test_experiment.py b/tests/test_experiment.py index e7e846fc..f654ab68 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -1,163 +1,90 @@ -import shutil -from contextlib import contextmanager -from pathlib import Path - import numpy as np import pytest import yaml from experanto.experiment import Experiment -EXPERIMENT_ROOT = Path("tests/experiment_data") - - -@contextmanager -def create_two_device_experiment( - device0_start=0.0, - device0_end=10.0, - device1_start=1.0, - device1_end=8.0, - sampling_rate=10.0, -): - """Create a temporary experiment with two sequence devices with different time ranges.""" - try: - for device_name, start, end in [ - ("device_0", device0_start, device0_end), - ("device_1", device1_start, device1_end), - ]: - device_root = EXPERIMENT_ROOT / device_name - (device_root / "meta").mkdir(parents=True, exist_ok=True) - - n_samples = int((end - start) * sampling_rate) + 1 - timestamps = np.linspace(start, end, n_samples) - data = np.random.rand(n_samples, 5) - - np.save(device_root / "timestamps.npy", timestamps) - np.save(device_root / "data.npy", data) - - meta = { - "start_time": start, - "end_time": end, - "modality": "sequence", - "sampling_rate": sampling_rate, - "phase_shift_per_signal": False, - "is_mem_mapped": False, - "n_signals": 5, - "n_timestamps": n_samples, - "dtype": "float64", - } - with open(device_root / "meta.yml", "w") as f: - yaml.safe_dump(meta, f) - - yield EXPERIMENT_ROOT - - finally: - shutil.rmtree(EXPERIMENT_ROOT) - - -@contextmanager -def create_three_device_experiment(): - """Create a temporary experiment with three sequence devices with different time ranges.""" - try: - for device_name, start, end in [ - ("device_0", 0.0, 10.0), - ("device_1", 1.0, 8.0), - ("device_2", 2.0, 9.0), - ]: - device_root = EXPERIMENT_ROOT / device_name - (device_root / "meta").mkdir(parents=True, exist_ok=True) - - sampling_rate = 10.0 - n_samples = int((end - start) * sampling_rate) + 1 - timestamps = np.linspace(start, end, n_samples) - data = np.random.rand(n_samples, 5) - - np.save(device_root / "timestamps.npy", timestamps) - np.save(device_root / "data.npy", data) - - meta = { - "start_time": start, - "end_time": end, - "modality": "sequence", - "sampling_rate": sampling_rate, - "phase_shift_per_signal": False, - "is_mem_mapped": False, - "n_signals": 5, - "n_timestamps": n_samples, - "dtype": "float64", - } - with open(device_root / "meta.yml", "w") as f: - yaml.safe_dump(meta, f) - - yield EXPERIMENT_ROOT - - finally: - shutil.rmtree(EXPERIMENT_ROOT) - - -def get_two_device_config(): + +def make_sequence_device(root, name, start, end, sampling_rate=10.0): + """Create a single sequence device folder under root.""" + device_root = root / name + (device_root / "meta").mkdir(parents=True, exist_ok=True) + + n_samples = int((end - start) * sampling_rate) + 1 + timestamps = np.linspace(start, end, n_samples) + data = np.random.rand(n_samples, 5) + + np.save(device_root / "timestamps.npy", timestamps) + np.save(device_root / "data.npy", data) + + meta = { + "start_time": start, + "end_time": end, + "modality": "sequence", + "sampling_rate": sampling_rate, + "phase_shift_per_signal": False, + "is_mem_mapped": False, + "n_signals": 5, + "n_timestamps": n_samples, + "dtype": "float64", + } + with open(device_root / "meta.yml", "w") as f: + yaml.safe_dump(meta, f) + + +def make_config(*device_names): return { - "device_0": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}, - "device_1": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}, + name: {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}} + for name in device_names } -def test_experiment_start_end_time_reflects_union(): +def test_experiment_start_end_time_reflects_union(tmp_path): """ - Experiment.start_time and end_time should reflect the union - of all device time ranges — the earliest start and latest end - across all devices, not just the last loaded device. + Experiment.start_time and end_time should reflect the union of all + device time ranges, earliest start and latest end across all devices. """ - with create_two_device_experiment( - device0_start=1.0, - device0_end=8.0, - device1_start=0.0, - device1_end=10.0, - ) as experiment_path: - experiment = Experiment( - root_folder=experiment_path, - modality_config=get_two_device_config(), - ) - - # Union: start = min(1.0, 0.0) = 0.0, end = max(8.0, 10.0) = 10.0 - assert experiment.start_time == pytest.approx( - 0.0 - ), f"Expected start_time=0.0, got {experiment.start_time}" - assert experiment.end_time == pytest.approx( - 10.0 - ), f"Expected end_time=10.0, got {experiment.end_time}" - - -def test_experiment_single_device_time_range(): + make_sequence_device(tmp_path, "device_0", start=1.0, end=8.0) + make_sequence_device(tmp_path, "device_1", start=0.0, end=10.0) + + experiment = Experiment( + root_folder=tmp_path, + modality_config=make_config("device_0", "device_1"), + ) + + # Union: start = min(1.0, 0.0) = 0.0, end = max(8.0, 10.0) = 10.0 + assert experiment.start_time == pytest.approx(0.0), ( + f"Expected start_time=0.0, got {experiment.start_time}" + ) + assert experiment.end_time == pytest.approx(10.0), ( + f"Expected end_time=10.0, got {experiment.end_time}" + ) + + +def test_experiment_single_device_time_range(tmp_path): """With a single device, start_time and end_time should match that device's range.""" - with create_two_device_experiment( - device0_start=2.0, - device0_end=9.0, - ) as experiment_path: - config = {"device_0": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}} - experiment = Experiment( - root_folder=experiment_path, - modality_config=config, - ) - - assert experiment.start_time == pytest.approx(2.0) - assert experiment.end_time == pytest.approx(9.0) - - -def test_experiment_start_end_time_three_devices(): - """With three devices, start_time and end_time should reflect the union of all three — - earliest start and latest end across all devices.""" - with create_three_device_experiment() as experiment_path: - config = { - "device_0": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}, - "device_1": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}, - "device_2": {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}}, - } - experiment = Experiment( - root_folder=experiment_path, - modality_config=config, - ) - - # Union: start = min(0.0, 1.0, 2.0) = 0.0, end = max(10.0, 8.0, 9.0) = 10.0 - assert experiment.start_time == pytest.approx(0.0) - assert experiment.end_time == pytest.approx(10.0) + make_sequence_device(tmp_path, "device_0", start=2.0, end=9.0) + + experiment = Experiment( + root_folder=tmp_path, + modality_config=make_config("device_0"), + ) + + assert experiment.start_time == pytest.approx(2.0) + assert experiment.end_time == pytest.approx(9.0) + + +def test_experiment_start_end_time_three_devices(tmp_path): + """With three devices, start_time and end_time should reflect the union of all three.""" + make_sequence_device(tmp_path, "device_0", start=0.0, end=10.0) + make_sequence_device(tmp_path, "device_1", start=1.0, end=8.0) + make_sequence_device(tmp_path, "device_2", start=2.0, end=9.0) + + experiment = Experiment( + root_folder=tmp_path, + modality_config=make_config("device_0", "device_1", "device_2"), + ) + + # Union: start = min(0.0, 1.0, 2.0) = 0.0, end = max(10.0, 8.0, 9.0) = 10.0 + assert experiment.start_time == pytest.approx(0.0) + assert experiment.end_time == pytest.approx(10.0) \ No newline at end of file From 118a92845546251c47416f653838c97444e08199 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 16 Mar 2026 10:47:45 +0000 Subject: [PATCH 03/24] style: auto-format with black and isort --- tests/test_experiment.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_experiment.py b/tests/test_experiment.py index f654ab68..c4a36b20 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -53,12 +53,12 @@ def test_experiment_start_end_time_reflects_union(tmp_path): ) # Union: start = min(1.0, 0.0) = 0.0, end = max(8.0, 10.0) = 10.0 - assert experiment.start_time == pytest.approx(0.0), ( - f"Expected start_time=0.0, got {experiment.start_time}" - ) - assert experiment.end_time == pytest.approx(10.0), ( - f"Expected end_time=10.0, got {experiment.end_time}" - ) + assert experiment.start_time == pytest.approx( + 0.0 + ), f"Expected start_time=0.0, got {experiment.start_time}" + assert experiment.end_time == pytest.approx( + 10.0 + ), f"Expected end_time=10.0, got {experiment.end_time}" def test_experiment_single_device_time_range(tmp_path): @@ -87,4 +87,4 @@ def test_experiment_start_end_time_three_devices(tmp_path): # Union: start = min(0.0, 1.0, 2.0) = 0.0, end = max(10.0, 8.0, 9.0) = 10.0 assert experiment.start_time == pytest.approx(0.0) - assert experiment.end_time == pytest.approx(10.0) \ No newline at end of file + assert experiment.end_time == pytest.approx(10.0) From 36e67074c38b096b7d5bba89745dd6a09b9bbcbf Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:32:11 +0530 Subject: [PATCH 04/24] fix: add None guard for dev.start_time and end_time in _load_devices --- experanto/experiment.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index 22ea9a5d..d3c6e7ba 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -121,8 +121,10 @@ def _load_devices(self) -> None: ) self.devices[d.name] = dev - self.start_time = min(self.start_time, dev.start_time) - self.end_time = max(self.end_time, dev.end_time) + if dev.start_time is not None: + self.start_time = min(self.start_time, dev.start_time) + if dev.end_time is not None: + self.end_time = max(self.end_time, dev.end_time) logger.info("Parsing finished") @property From 45c68817391266fc826c05bbcb1a161f147b140e Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:44:16 +0530 Subject: [PATCH 05/24] test: separate data creation helpers into create_experiment.py --- tests/create_experiment.py | 36 +++++++++++++++++++++++ tests/test_experiment.py | 58 ++++++++------------------------------ 2 files changed, 48 insertions(+), 46 deletions(-) create mode 100644 tests/create_experiment.py diff --git a/tests/create_experiment.py b/tests/create_experiment.py new file mode 100644 index 00000000..c1fd3cf9 --- /dev/null +++ b/tests/create_experiment.py @@ -0,0 +1,36 @@ +import numpy as np +import yaml + + +def make_sequence_device(root, name, start, end, sampling_rate=10.0): + """Create a single sequence device folder under root.""" + device_root = root / name + (device_root / "meta").mkdir(parents=True, exist_ok=True) + + n_samples = int((end - start) * sampling_rate) + 1 + timestamps = np.linspace(start, end, n_samples) + data = np.random.rand(n_samples, 5) + + np.save(device_root / "timestamps.npy", timestamps) + np.save(device_root / "data.npy", data) + + meta = { + "start_time": start, + "end_time": end, + "modality": "sequence", + "sampling_rate": sampling_rate, + "phase_shift_per_signal": False, + "is_mem_mapped": False, + "n_signals": 5, + "n_timestamps": n_samples, + "dtype": "float64", + } + with open(device_root / "meta.yml", "w") as f: + yaml.safe_dump(meta, f) + + +def make_modality_config(*device_names): + return { + name: {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}} + for name in device_names + } \ No newline at end of file diff --git a/tests/test_experiment.py b/tests/test_experiment.py index c4a36b20..b6a2798c 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -1,64 +1,30 @@ import numpy as np import pytest -import yaml from experanto.experiment import Experiment - - -def make_sequence_device(root, name, start, end, sampling_rate=10.0): - """Create a single sequence device folder under root.""" - device_root = root / name - (device_root / "meta").mkdir(parents=True, exist_ok=True) - - n_samples = int((end - start) * sampling_rate) + 1 - timestamps = np.linspace(start, end, n_samples) - data = np.random.rand(n_samples, 5) - - np.save(device_root / "timestamps.npy", timestamps) - np.save(device_root / "data.npy", data) - - meta = { - "start_time": start, - "end_time": end, - "modality": "sequence", - "sampling_rate": sampling_rate, - "phase_shift_per_signal": False, - "is_mem_mapped": False, - "n_signals": 5, - "n_timestamps": n_samples, - "dtype": "float64", - } - with open(device_root / "meta.yml", "w") as f: - yaml.safe_dump(meta, f) - - -def make_config(*device_names): - return { - name: {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}} - for name in device_names - } +from .create_experiment import make_sequence_device, make_modality_config def test_experiment_start_end_time_reflects_union(tmp_path): """ Experiment.start_time and end_time should reflect the union of all - device time ranges, earliest start and latest end across all devices. + device time ranges — earliest start and latest end across all devices. """ make_sequence_device(tmp_path, "device_0", start=1.0, end=8.0) make_sequence_device(tmp_path, "device_1", start=0.0, end=10.0) experiment = Experiment( root_folder=tmp_path, - modality_config=make_config("device_0", "device_1"), + modality_config=make_modality_config("device_0", "device_1"), ) # Union: start = min(1.0, 0.0) = 0.0, end = max(8.0, 10.0) = 10.0 - assert experiment.start_time == pytest.approx( - 0.0 - ), f"Expected start_time=0.0, got {experiment.start_time}" - assert experiment.end_time == pytest.approx( - 10.0 - ), f"Expected end_time=10.0, got {experiment.end_time}" + assert experiment.start_time == pytest.approx(0.0), ( + f"Expected start_time=0.0, got {experiment.start_time}" + ) + assert experiment.end_time == pytest.approx(10.0), ( + f"Expected end_time=10.0, got {experiment.end_time}" + ) def test_experiment_single_device_time_range(tmp_path): @@ -67,7 +33,7 @@ def test_experiment_single_device_time_range(tmp_path): experiment = Experiment( root_folder=tmp_path, - modality_config=make_config("device_0"), + modality_config=make_modality_config("device_0"), ) assert experiment.start_time == pytest.approx(2.0) @@ -82,9 +48,9 @@ def test_experiment_start_end_time_three_devices(tmp_path): experiment = Experiment( root_folder=tmp_path, - modality_config=make_config("device_0", "device_1", "device_2"), + modality_config=make_modality_config("device_0", "device_1", "device_2"), ) # Union: start = min(0.0, 1.0, 2.0) = 0.0, end = max(10.0, 8.0, 9.0) = 10.0 assert experiment.start_time == pytest.approx(0.0) - assert experiment.end_time == pytest.approx(10.0) + assert experiment.end_time == pytest.approx(10.0) \ No newline at end of file From 38153ad3270a38651ff0b3beb8183646fe1a8268 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 16 Mar 2026 11:14:54 +0000 Subject: [PATCH 06/24] style: auto-format with black and isort --- tests/create_experiment.py | 2 +- tests/test_experiment.py | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/create_experiment.py b/tests/create_experiment.py index c1fd3cf9..8650bc72 100644 --- a/tests/create_experiment.py +++ b/tests/create_experiment.py @@ -33,4 +33,4 @@ def make_modality_config(*device_names): return { name: {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}} for name in device_names - } \ No newline at end of file + } diff --git a/tests/test_experiment.py b/tests/test_experiment.py index b6a2798c..306f5eb6 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -2,7 +2,8 @@ import pytest from experanto.experiment import Experiment -from .create_experiment import make_sequence_device, make_modality_config + +from .create_experiment import make_modality_config, make_sequence_device def test_experiment_start_end_time_reflects_union(tmp_path): @@ -19,12 +20,12 @@ def test_experiment_start_end_time_reflects_union(tmp_path): ) # Union: start = min(1.0, 0.0) = 0.0, end = max(8.0, 10.0) = 10.0 - assert experiment.start_time == pytest.approx(0.0), ( - f"Expected start_time=0.0, got {experiment.start_time}" - ) - assert experiment.end_time == pytest.approx(10.0), ( - f"Expected end_time=10.0, got {experiment.end_time}" - ) + assert experiment.start_time == pytest.approx( + 0.0 + ), f"Expected start_time=0.0, got {experiment.start_time}" + assert experiment.end_time == pytest.approx( + 10.0 + ), f"Expected end_time=10.0, got {experiment.end_time}" def test_experiment_single_device_time_range(tmp_path): @@ -53,4 +54,4 @@ def test_experiment_start_end_time_three_devices(tmp_path): # Union: start = min(0.0, 1.0, 2.0) = 0.0, end = max(10.0, 8.0, 9.0) = 10.0 assert experiment.start_time == pytest.approx(0.0) - assert experiment.end_time == pytest.approx(10.0) \ No newline at end of file + assert experiment.end_time == pytest.approx(10.0) From 791bb98c5bb5b22020ed37c1722d91b0d21cf2e4 Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:30:57 +0530 Subject: [PATCH 07/24] fix: improve None handling with warning and add finite range validation --- experanto/experiment.py | 18 ++++++++++++++++-- tests/test_experiment.py | 1 - 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index d3c6e7ba..3b6ddbe5 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -121,11 +121,25 @@ def _load_devices(self) -> None: ) self.devices[d.name] = dev - if dev.start_time is not None: + if dev.start_time is None or dev.end_time is None: + logger.warning( + "Device %s has undefined start_time or end_time and will be " + "excluded from the experiment-wide time range.", + d.name, + ) + else: self.start_time = min(self.start_time, dev.start_time) - if dev.end_time is not None: self.end_time = max(self.end_time, dev.end_time) logger.info("Parsing finished") + if not self.devices: + logger.warning( + "No devices were loaded. Please check your root folder %s", self.root_folder + ) + elif not np.isfinite(self.start_time) and np.isfinite(self.end_time): + raise ValueError( + "Experiment time range could not be determined: at least one device " + "must define finite start_time and end_time." + ) @property def device_names(self): diff --git a/tests/test_experiment.py b/tests/test_experiment.py index 306f5eb6..c37fb00f 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -1,4 +1,3 @@ -import numpy as np import pytest from experanto.experiment import Experiment From d6737de6653402383a5cf320caf630b2f715c828 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 16 Mar 2026 14:13:35 +0000 Subject: [PATCH 08/24] style: auto-format with black and isort --- experanto/experiment.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index 3b6ddbe5..efa258bc 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -125,7 +125,7 @@ def _load_devices(self) -> None: logger.warning( "Device %s has undefined start_time or end_time and will be " "excluded from the experiment-wide time range.", - d.name, + d.name, ) else: self.start_time = min(self.start_time, dev.start_time) @@ -133,7 +133,8 @@ def _load_devices(self) -> None: logger.info("Parsing finished") if not self.devices: logger.warning( - "No devices were loaded. Please check your root folder %s", self.root_folder + "No devices were loaded. Please check your root folder %s", + self.root_folder, ) elif not np.isfinite(self.start_time) and np.isfinite(self.end_time): raise ValueError( From 7c0775483c5cbf98cb9178da511c58a53e1c5be6 Mon Sep 17 00:00:00 2001 From: Reema Hanim Hanass <220374295+binary69@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:51:33 +0530 Subject: [PATCH 09/24] fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- experanto/experiment.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index efa258bc..c55fc18e 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -131,16 +131,17 @@ def _load_devices(self) -> None: self.start_time = min(self.start_time, dev.start_time) self.end_time = max(self.end_time, dev.end_time) logger.info("Parsing finished") - if not self.devices: - logger.warning( - "No devices were loaded. Please check your root folder %s", - self.root_folder, - ) - elif not np.isfinite(self.start_time) and np.isfinite(self.end_time): - raise ValueError( - "Experiment time range could not be determined: at least one device " - "must define finite start_time and end_time." - ) + + if not self.devices: + logger.warning( + "No devices were loaded. Please check your root folder %s", + self.root_folder, + ) + elif not np.isfinite(self.start_time) and np.isfinite(self.end_time): + raise ValueError( + "Experiment time range could not be determined: at least one device " + "must define finite start_time and end_time." + ) @property def device_names(self): From dbf1896e544ac4f90ab4baefe4b117a89aa257ae Mon Sep 17 00:00:00 2001 From: Reema Hanim Hanass <220374295+binary69@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:58:00 +0530 Subject: [PATCH 10/24] fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- experanto/experiment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index c55fc18e..5224cfa7 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -137,7 +137,8 @@ def _load_devices(self) -> None: "No devices were loaded. Please check your root folder %s", self.root_folder, ) - elif not np.isfinite(self.start_time) and np.isfinite(self.end_time): + elif (not (np.isfinite(self.start_time) and np.isfinite(self.end_time)) + or self.start_time > self.end_time): raise ValueError( "Experiment time range could not be determined: at least one device " "must define finite start_time and end_time." From 55e8f2e408631ff843ba163102d3d3aab9884828 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 16 Mar 2026 14:28:27 +0000 Subject: [PATCH 11/24] style: auto-format with black and isort --- experanto/experiment.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index 5224cfa7..e9934858 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -137,8 +137,10 @@ def _load_devices(self) -> None: "No devices were loaded. Please check your root folder %s", self.root_folder, ) - elif (not (np.isfinite(self.start_time) and np.isfinite(self.end_time)) - or self.start_time > self.end_time): + elif ( + not (np.isfinite(self.start_time) and np.isfinite(self.end_time)) + or self.start_time > self.end_time + ): raise ValueError( "Experiment time range could not be determined: at least one device " "must define finite start_time and end_time." From a06829074369ce71a30aa83c31ebfa19c699864a Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:50:59 +0530 Subject: [PATCH 12/24] style: reformat elif condition to Black-compliant style --- experanto/experiment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index e9934858..bebf9fc3 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -138,7 +138,8 @@ def _load_devices(self) -> None: self.root_folder, ) elif ( - not (np.isfinite(self.start_time) and np.isfinite(self.end_time)) + not np.isfinite(self.start_time) + or not np.isfinite(self.end_time) or self.start_time > self.end_time ): raise ValueError( From ae606bd6025d2615b46a3eea74516b2d74cd5614 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 16 Mar 2026 17:21:59 +0000 Subject: [PATCH 13/24] style: auto-format with black and isort --- experanto/experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index bebf9fc3..433b8f2e 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -138,7 +138,7 @@ def _load_devices(self) -> None: self.root_folder, ) elif ( - not np.isfinite(self.start_time) + not np.isfinite(self.start_time) or not np.isfinite(self.end_time) or self.start_time > self.end_time ): From 929c92d84b7b7f849b6cec4399a7a5d8ed22e208 Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:37:36 +0530 Subject: [PATCH 14/24] fix: experiment time range validation and test refactor --- experanto/experiment.py | 15 +++---- tests/create_experiment.py | 63 +++++++++++++++----------- tests/test_experiment.py | 90 +++++++++++++++++++++----------------- 3 files changed, 94 insertions(+), 74 deletions(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index 433b8f2e..7f149de2 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -120,8 +120,7 @@ def _load_devices(self) -> None: **interp_conf, # type: ignore[arg-type] ) - self.devices[d.name] = dev - if dev.start_time is None or dev.end_time is None: + if dev.start_time is None or dev.end_time is None or np.isinf(dev.start_time) or np.isinf(dev.end_time): logger.warning( "Device %s has undefined start_time or end_time and will be " "excluded from the experiment-wide time range.", @@ -130,18 +129,14 @@ def _load_devices(self) -> None: else: self.start_time = min(self.start_time, dev.start_time) self.end_time = max(self.end_time, dev.end_time) + self.devices[d.name] = dev logger.info("Parsing finished") if not self.devices: - logger.warning( - "No devices were loaded. Please check your root folder %s", - self.root_folder, + raise ValueError( + "Experiment time range could not be determined: no devices with valid start_time and end_time were found." ) - elif ( - not np.isfinite(self.start_time) - or not np.isfinite(self.end_time) - or self.start_time > self.end_time - ): + elif self.start_time > self.end_time: raise ValueError( "Experiment time range could not be determined: at least one device " "must define finite start_time and end_time." diff --git a/tests/create_experiment.py b/tests/create_experiment.py index 8650bc72..4fe53342 100644 --- a/tests/create_experiment.py +++ b/tests/create_experiment.py @@ -1,36 +1,49 @@ +import shutil +from contextlib import contextmanager +from pathlib import Path + import numpy as np import yaml -def make_sequence_device(root, name, start, end, sampling_rate=10.0): +@contextmanager +def make_sequence_device(root, name, start, end, sampling_rate=10.0, n_signals=5, override_meta=None): """Create a single sequence device folder under root.""" device_root = root / name - (device_root / "meta").mkdir(parents=True, exist_ok=True) - - n_samples = int((end - start) * sampling_rate) + 1 - timestamps = np.linspace(start, end, n_samples) - data = np.random.rand(n_samples, 5) - - np.save(device_root / "timestamps.npy", timestamps) - np.save(device_root / "data.npy", data) - - meta = { - "start_time": start, - "end_time": end, - "modality": "sequence", - "sampling_rate": sampling_rate, - "phase_shift_per_signal": False, - "is_mem_mapped": False, - "n_signals": 5, - "n_timestamps": n_samples, - "dtype": "float64", - } - with open(device_root / "meta.yml", "w") as f: - yaml.safe_dump(meta, f) + try: + (device_root / "meta").mkdir(parents=True, exist_ok=True) + + n_samples = int((end - start) * sampling_rate) + 1 # +1 to include both start and end as sample points + timestamps = np.linspace(start, end, n_samples) + data = np.random.rand(n_samples, n_signals) + + np.save(device_root / "timestamps.npy", timestamps) + np.save(device_root / "data.npy", data) + + meta = { + "start_time": start, + "end_time": end, + "modality": "sequence", + "sampling_rate": sampling_rate, + "phase_shift_per_signal": False, + "is_mem_mapped": False, + "n_signals": n_signals, + "n_timestamps": n_samples, + "dtype": "float64", + } + if override_meta: + meta.update(override_meta) + with open(device_root / "meta.yml", "w") as f: + yaml.safe_dump(meta, f) + + yield device_root + + finally: + shutil.rmtree(device_root) -def make_modality_config(*device_names): +def make_modality_config(*device_names, sampling_rate=10.0, offset=0.0): return { - name: {"interpolation": {"sampling_rate": 10.0, "offset": 0.0}} + name: {"interpolation": {"sampling_rate": sampling_rate, "offset": offset}} for name in device_names } diff --git a/tests/test_experiment.py b/tests/test_experiment.py index c37fb00f..0832a542 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -1,56 +1,68 @@ +from contextlib import ExitStack + import pytest from experanto.experiment import Experiment from .create_experiment import make_modality_config, make_sequence_device +DEVICE_TIME_RANGE_CASES = [ + # Single device: start and end should match that device's range + ([(2.0, 9.0)], 2.0, 9.0), + # Two devices with different ranges: start should be min, end should be max + ([(1.0, 8.0), (0.0, 10.0)], 0.0, 10.0), + # Three devices with different ranges: start should be min, end should be max + ([(0.0, 10.0), (1.0, 8.0), (2.0, 9.0)], 0.0, 10.0), +] + +INVALID_META_CASES = [ + {"start_time": None, "end_time": None}, # Both missing + {"start_time": None, "end_time": 10.0}, # Missing start_time + {"start_time": 0.0, "end_time": None}, # Missing end_time + {"start_time": float("inf"), "end_time": 10.0}, # Infinite start_time + {"start_time": 0.0, "end_time": float("inf")}, # Infinite end_time + {"start_time": 5.0, "end_time": 2.0}, # start_time > end_time +] -def test_experiment_start_end_time_reflects_union(tmp_path): +@pytest.mark.parametrize("n_signals",[5,20]) +@pytest.mark.parametrize("device_ranges, expected_start, expected_end", DEVICE_TIME_RANGE_CASES) +def test_experiment_start_end_time_reflects_union(tmp_path, device_ranges, expected_start, expected_end, n_signals): """ Experiment.start_time and end_time should reflect the union of all device time ranges — earliest start and latest end across all devices. """ - make_sequence_device(tmp_path, "device_0", start=1.0, end=8.0) - make_sequence_device(tmp_path, "device_1", start=0.0, end=10.0) - - experiment = Experiment( - root_folder=tmp_path, - modality_config=make_modality_config("device_0", "device_1"), - ) - - # Union: start = min(1.0, 0.0) = 0.0, end = max(8.0, 10.0) = 10.0 - assert experiment.start_time == pytest.approx( - 0.0 - ), f"Expected start_time=0.0, got {experiment.start_time}" - assert experiment.end_time == pytest.approx( - 10.0 - ), f"Expected end_time=10.0, got {experiment.end_time}" + device_names = [f"device_{i}" for i in range(len(device_ranges))] + with ExitStack() as stack: + for name, (start, end) in zip(device_names, device_ranges): + stack.enter_context(make_sequence_device(tmp_path, name, start=start, end=end, n_signals=n_signals)) -def test_experiment_single_device_time_range(tmp_path): - """With a single device, start_time and end_time should match that device's range.""" - make_sequence_device(tmp_path, "device_0", start=2.0, end=9.0) + experiment = Experiment( + root_folder=tmp_path, + modality_config=make_modality_config(*device_names), + ) - experiment = Experiment( - root_folder=tmp_path, - modality_config=make_modality_config("device_0"), + assert experiment.start_time == pytest.approx(expected_start), ( + f"Expected start_time={expected_start}, got {experiment.start_time}" ) - - assert experiment.start_time == pytest.approx(2.0) - assert experiment.end_time == pytest.approx(9.0) - - -def test_experiment_start_end_time_three_devices(tmp_path): - """With three devices, start_time and end_time should reflect the union of all three.""" - make_sequence_device(tmp_path, "device_0", start=0.0, end=10.0) - make_sequence_device(tmp_path, "device_1", start=1.0, end=8.0) - make_sequence_device(tmp_path, "device_2", start=2.0, end=9.0) - - experiment = Experiment( - root_folder=tmp_path, - modality_config=make_modality_config("device_0", "device_1", "device_2"), + assert experiment.end_time == pytest.approx(expected_end), ( + f"Expected end_time={expected_end}, got {experiment.end_time}" ) - # Union: start = min(0.0, 1.0, 2.0) = 0.0, end = max(10.0, 8.0, 9.0) = 10.0 - assert experiment.start_time == pytest.approx(0.0) - assert experiment.end_time == pytest.approx(10.0) +@pytest.mark.parametrize("override_meta", INVALID_META_CASES) +def test_experiment_invalid_metadata(tmp_path, override_meta): + """ + Experiment should raise an error when initialized with invalid metadata. + """ + with make_sequence_device( + tmp_path, + "device_0", + start=0.0, + end=10.0, + override_meta=override_meta, + ): + with pytest.raises(ValueError, match="Experiment time range could not be determined"): + Experiment( + root_folder=tmp_path, + modality_config=make_modality_config("device_0"), + ) \ No newline at end of file From a7edf85dd7f7e11be4bc71db549d3d7791e62d47 Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:12:01 +0530 Subject: [PATCH 15/24] fix: remove unused imports flagged by ruff --- experanto/experiment.py | 4 +--- tests/create_experiment.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index 7f149de2..51145d8b 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -1,11 +1,9 @@ from __future__ import annotations import logging -import re import warnings -from collections.abc import Sequence from pathlib import Path -from typing import Optional, Union +from typing import Union import numpy as np from hydra.utils import instantiate diff --git a/tests/create_experiment.py b/tests/create_experiment.py index 4fe53342..9011c7ec 100644 --- a/tests/create_experiment.py +++ b/tests/create_experiment.py @@ -1,6 +1,5 @@ import shutil from contextlib import contextmanager -from pathlib import Path import numpy as np import yaml From 423f86f0fddde78e786d15f5b5beac7abbde9ef1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 17 Mar 2026 18:08:40 +0000 Subject: [PATCH 16/24] style: auto-format with black and isort --- experanto/experiment.py | 7 ++++++- tests/create_experiment.py | 8 ++++++-- tests/test_experiment.py | 42 ++++++++++++++++++++++++-------------- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index 51145d8b..046ee10a 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -118,7 +118,12 @@ def _load_devices(self) -> None: **interp_conf, # type: ignore[arg-type] ) - if dev.start_time is None or dev.end_time is None or np.isinf(dev.start_time) or np.isinf(dev.end_time): + if ( + dev.start_time is None + or dev.end_time is None + or np.isinf(dev.start_time) + or np.isinf(dev.end_time) + ): logger.warning( "Device %s has undefined start_time or end_time and will be " "excluded from the experiment-wide time range.", diff --git a/tests/create_experiment.py b/tests/create_experiment.py index 9011c7ec..620dc7cf 100644 --- a/tests/create_experiment.py +++ b/tests/create_experiment.py @@ -6,13 +6,17 @@ @contextmanager -def make_sequence_device(root, name, start, end, sampling_rate=10.0, n_signals=5, override_meta=None): +def make_sequence_device( + root, name, start, end, sampling_rate=10.0, n_signals=5, override_meta=None +): """Create a single sequence device folder under root.""" device_root = root / name try: (device_root / "meta").mkdir(parents=True, exist_ok=True) - n_samples = int((end - start) * sampling_rate) + 1 # +1 to include both start and end as sample points + n_samples = ( + int((end - start) * sampling_rate) + 1 + ) # +1 to include both start and end as sample points timestamps = np.linspace(start, end, n_samples) data = np.random.rand(n_samples, n_signals) diff --git a/tests/test_experiment.py b/tests/test_experiment.py index 0832a542..bae51520 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -18,15 +18,20 @@ INVALID_META_CASES = [ {"start_time": None, "end_time": None}, # Both missing {"start_time": None, "end_time": 10.0}, # Missing start_time - {"start_time": 0.0, "end_time": None}, # Missing end_time + {"start_time": 0.0, "end_time": None}, # Missing end_time {"start_time": float("inf"), "end_time": 10.0}, # Infinite start_time - {"start_time": 0.0, "end_time": float("inf")}, # Infinite end_time - {"start_time": 5.0, "end_time": 2.0}, # start_time > end_time + {"start_time": 0.0, "end_time": float("inf")}, # Infinite end_time + {"start_time": 5.0, "end_time": 2.0}, # start_time > end_time ] -@pytest.mark.parametrize("n_signals",[5,20]) -@pytest.mark.parametrize("device_ranges, expected_start, expected_end", DEVICE_TIME_RANGE_CASES) -def test_experiment_start_end_time_reflects_union(tmp_path, device_ranges, expected_start, expected_end, n_signals): + +@pytest.mark.parametrize("n_signals", [5, 20]) +@pytest.mark.parametrize( + "device_ranges, expected_start, expected_end", DEVICE_TIME_RANGE_CASES +) +def test_experiment_start_end_time_reflects_union( + tmp_path, device_ranges, expected_start, expected_end, n_signals +): """ Experiment.start_time and end_time should reflect the union of all device time ranges — earliest start and latest end across all devices. @@ -35,19 +40,24 @@ def test_experiment_start_end_time_reflects_union(tmp_path, device_ranges, expec with ExitStack() as stack: for name, (start, end) in zip(device_names, device_ranges): - stack.enter_context(make_sequence_device(tmp_path, name, start=start, end=end, n_signals=n_signals)) + stack.enter_context( + make_sequence_device( + tmp_path, name, start=start, end=end, n_signals=n_signals + ) + ) experiment = Experiment( root_folder=tmp_path, modality_config=make_modality_config(*device_names), ) - assert experiment.start_time == pytest.approx(expected_start), ( - f"Expected start_time={expected_start}, got {experiment.start_time}" - ) - assert experiment.end_time == pytest.approx(expected_end), ( - f"Expected end_time={expected_end}, got {experiment.end_time}" - ) + assert experiment.start_time == pytest.approx( + expected_start + ), f"Expected start_time={expected_start}, got {experiment.start_time}" + assert experiment.end_time == pytest.approx( + expected_end + ), f"Expected end_time={expected_end}, got {experiment.end_time}" + @pytest.mark.parametrize("override_meta", INVALID_META_CASES) def test_experiment_invalid_metadata(tmp_path, override_meta): @@ -61,8 +71,10 @@ def test_experiment_invalid_metadata(tmp_path, override_meta): end=10.0, override_meta=override_meta, ): - with pytest.raises(ValueError, match="Experiment time range could not be determined"): + with pytest.raises( + ValueError, match="Experiment time range could not be determined" + ): Experiment( root_folder=tmp_path, modality_config=make_modality_config("device_0"), - ) \ No newline at end of file + ) From 86dc0bcd712beec123afb8d9979b83a87f218dab Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:59:07 +0530 Subject: [PATCH 17/24] fix: replace np.isinf with np.isfinite to catch NaN and -inf values --- experanto/experiment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index 046ee10a..897e3021 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -121,8 +121,8 @@ def _load_devices(self) -> None: if ( dev.start_time is None or dev.end_time is None - or np.isinf(dev.start_time) - or np.isinf(dev.end_time) + or not np.isfinite(dev.start_time) + or not np.isfinite(dev.end_time) ): logger.warning( "Device %s has undefined start_time or end_time and will be " From 3f4aa6ee47ad06146a86bc58a6baf9518a3d4291 Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:00:47 +0530 Subject: [PATCH 18/24] fix: make sampling_rates and offsets per-device lists in make_modality_config --- tests/create_experiment.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/create_experiment.py b/tests/create_experiment.py index 620dc7cf..66c49f6c 100644 --- a/tests/create_experiment.py +++ b/tests/create_experiment.py @@ -45,8 +45,20 @@ def make_sequence_device( shutil.rmtree(device_root) -def make_modality_config(*device_names, sampling_rate=10.0, offset=0.0): +def make_modality_config(*device_names, sampling_rates=None, offsets=None): + if sampling_rates is None: + sampling_rates = [10.0] * len(device_names) + if offsets is None: + offsets = [0.0] * len(device_names) + + assert len(device_names) == len(sampling_rates), ( + f"sampling_rates length {len(sampling_rates)} does not match device_names length {len(device_names)}" + ) + assert len(device_names) == len(offsets), ( + f"offsets length {len(offsets)} does not match device_names length {len(device_names)}" + ) + return { - name: {"interpolation": {"sampling_rate": sampling_rate, "offset": offset}} - for name in device_names + name: {"interpolation": {"sampling_rate": sr, "offset": off}} + for name, sr, off in zip(device_names, sampling_rates, offsets) } From 2e3cddf1653c24a6c916016a63d16405be36be5c Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:01:08 +0530 Subject: [PATCH 19/24] fix: expand test coverage with edge cases, parametrize invalid device skip test and add caplog assertion --- tests/test_experiment.py | 111 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 3 deletions(-) diff --git a/tests/test_experiment.py b/tests/test_experiment.py index bae51520..2710cf0f 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -1,3 +1,4 @@ +import logging from contextlib import ExitStack import pytest @@ -13,21 +14,56 @@ ([(1.0, 8.0), (0.0, 10.0)], 0.0, 10.0), # Three devices with different ranges: start should be min, end should be max ([(0.0, 10.0), (1.0, 8.0), (2.0, 9.0)], 0.0, 10.0), + # Devices with non-overlapping ranges: start should be min, end should be max + ([(0.0, 3.0), (7.0, 8.0)], 0.0, 8.0), + # Devices with identical ranges: start and end should match that range + ([(1.0, 5.0), (1.0, 5.0)], 1.0, 5.0), + # Large time stamps: start should be min, end should be max + ([(1e9, 1e9 + 100), (1e9 - 50, 1e9 + 50)], 1e9 - 50, 1e9 + 100), ] +DEVICE_TIME_RANGE_IDS = [ + "single_device", + "two_devices_different_ranges", + "three_devices_different_ranges", + "non_overlapping_ranges", + "identical_ranges", + "large_time_stamps" +] + +# Inverted range is intentionally separate from INVALID_META_CASES — +# None/NaN/inf are caught per-device before being added to self.devices, +# whereas start > end is only caught after all devices are loaded. INVALID_META_CASES = [ {"start_time": None, "end_time": None}, # Both missing {"start_time": None, "end_time": 10.0}, # Missing start_time {"start_time": 0.0, "end_time": None}, # Missing end_time {"start_time": float("inf"), "end_time": 10.0}, # Infinite start_time {"start_time": 0.0, "end_time": float("inf")}, # Infinite end_time - {"start_time": 5.0, "end_time": 2.0}, # start_time > end_time + {"start_time": float("-inf"), "end_time": 10.0}, # Negative Infinite start_time + {"start_time": 0.0, "end_time": float("-inf")}, # Negative Infinite end_time + {"start_time": float("nan"), "end_time": 10.0}, # NaN start_time + {"start_time": 0.0, "end_time": float("nan")}, # NaN end_time ] +INVALID_META_IDS = [ + "both_missing", + "missing_start_time", + "missing_end_time", + "infinite_start_time", + "infinite_end_time", + "negative_infinite_start_time", + "negative_infinite_end_time", + "nan_start_time", + "nan_end_time" +] +# Test for union of device time ranges @pytest.mark.parametrize("n_signals", [5, 20]) @pytest.mark.parametrize( - "device_ranges, expected_start, expected_end", DEVICE_TIME_RANGE_CASES + "device_ranges, expected_start, expected_end", + DEVICE_TIME_RANGE_CASES, + ids=DEVICE_TIME_RANGE_IDS, ) def test_experiment_start_end_time_reflects_union( tmp_path, device_ranges, expected_start, expected_end, n_signals @@ -59,10 +95,12 @@ def test_experiment_start_end_time_reflects_union( ), f"Expected end_time={expected_end}, got {experiment.end_time}" -@pytest.mark.parametrize("override_meta", INVALID_META_CASES) +# Safety check +@pytest.mark.parametrize("override_meta", INVALID_META_CASES, ids=INVALID_META_IDS) def test_experiment_invalid_metadata(tmp_path, override_meta): """ Experiment should raise an error when initialized with invalid metadata. + Covers cases where start_time or end_time is None, NaN, or infinite. """ with make_sequence_device( tmp_path, @@ -78,3 +116,70 @@ def test_experiment_invalid_metadata(tmp_path, override_meta): root_folder=tmp_path, modality_config=make_modality_config("device_0"), ) + +def test_experiment_inverted_time_range_raises(tmp_path): + """ + Experiment should raise ValueError when start_time > end_time. + This is a separate guard from invalid metadata (None/NaN/inf) because it + only becomes apparent after all devices are loaded and the overall time range is computed. + """ + with make_sequence_device( + tmp_path, + "device_0", + start=0.0, + end=10.0, + override_meta={"start_time": 5.0, "end_time": 2.0}, + ): + with pytest.raises(ValueError, match="Experiment time range could not be determined"): + Experiment( + root_folder=tmp_path, + modality_config=make_modality_config("device_0"), + ) + +@pytest.mark.parametrize("override_meta", INVALID_META_CASES, ids=INVALID_META_IDS) +def test_experiment_skips_invalid_devices(tmp_path, override_meta, caplog): + """ + Experiment should skip devices with invalid start_time or end_time and + log a warning, but still initialize successfully if at least one valid + device is present. The experiment time range should reflect only the + valid device. + """ + with ExitStack() as stack: + # Valid device with proper metadata + stack.enter_context( + make_sequence_device( + tmp_path, + "valid_device", + start=0.0, + end=10.0, + ) + ) + # Invalid device with missing start_time and end_time + stack.enter_context( + make_sequence_device( + tmp_path, + "invalid_device", + start=0.0, + end=10.0, + override_meta=override_meta, + ) + ) + + with caplog.at_level(logging.WARNING, logger="experanto.experiment"): + experiment = Experiment( + root_folder=tmp_path, + modality_config=make_modality_config("valid_device", "invalid_device"), + ) + + assert "valid_device" in experiment.devices + assert "invalid_device" not in experiment.devices + + assert experiment.start_time == pytest.approx(0.0), ( + f"Expected start_time=0.0, got {experiment.start_time}" + ) + assert experiment.end_time == pytest.approx(10.0), ( + f"Expected end_time=10.0, got {experiment.end_time}" + ) + assert any("invalid_device" in message for message in caplog.messages),( + "Expected warning about invalid_device was skipped" + ) \ No newline at end of file From 3c2ccc3cc3178baad224afa2bcfbdff90bb6bde2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 18 Mar 2026 17:31:50 +0000 Subject: [PATCH 20/24] style: auto-format with black and isort --- tests/create_experiment.py | 12 ++++++------ tests/test_experiment.py | 35 ++++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/tests/create_experiment.py b/tests/create_experiment.py index 66c49f6c..48dd124c 100644 --- a/tests/create_experiment.py +++ b/tests/create_experiment.py @@ -51,12 +51,12 @@ def make_modality_config(*device_names, sampling_rates=None, offsets=None): if offsets is None: offsets = [0.0] * len(device_names) - assert len(device_names) == len(sampling_rates), ( - f"sampling_rates length {len(sampling_rates)} does not match device_names length {len(device_names)}" - ) - assert len(device_names) == len(offsets), ( - f"offsets length {len(offsets)} does not match device_names length {len(device_names)}" - ) + assert len(device_names) == len( + sampling_rates + ), f"sampling_rates length {len(sampling_rates)} does not match device_names length {len(device_names)}" + assert len(device_names) == len( + offsets + ), f"offsets length {len(offsets)} does not match device_names length {len(device_names)}" return { name: {"interpolation": {"sampling_rate": sr, "offset": off}} diff --git a/tests/test_experiment.py b/tests/test_experiment.py index 2710cf0f..c06e96cf 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -28,7 +28,7 @@ "three_devices_different_ranges", "non_overlapping_ranges", "identical_ranges", - "large_time_stamps" + "large_time_stamps", ] # Inverted range is intentionally separate from INVALID_META_CASES — @@ -55,13 +55,14 @@ "negative_infinite_start_time", "negative_infinite_end_time", "nan_start_time", - "nan_end_time" + "nan_end_time", ] + # Test for union of device time ranges @pytest.mark.parametrize("n_signals", [5, 20]) @pytest.mark.parametrize( - "device_ranges, expected_start, expected_end", + "device_ranges, expected_start, expected_end", DEVICE_TIME_RANGE_CASES, ids=DEVICE_TIME_RANGE_IDS, ) @@ -117,25 +118,29 @@ def test_experiment_invalid_metadata(tmp_path, override_meta): modality_config=make_modality_config("device_0"), ) + def test_experiment_inverted_time_range_raises(tmp_path): """ Experiment should raise ValueError when start_time > end_time. - This is a separate guard from invalid metadata (None/NaN/inf) because it + This is a separate guard from invalid metadata (None/NaN/inf) because it only becomes apparent after all devices are loaded and the overall time range is computed. """ with make_sequence_device( tmp_path, "device_0", start=0.0, - end=10.0, + end=10.0, override_meta={"start_time": 5.0, "end_time": 2.0}, ): - with pytest.raises(ValueError, match="Experiment time range could not be determined"): + with pytest.raises( + ValueError, match="Experiment time range could not be determined" + ): Experiment( root_folder=tmp_path, modality_config=make_modality_config("device_0"), ) + @pytest.mark.parametrize("override_meta", INVALID_META_CASES, ids=INVALID_META_IDS) def test_experiment_skips_invalid_devices(tmp_path, override_meta, caplog): """ @@ -174,12 +179,12 @@ def test_experiment_skips_invalid_devices(tmp_path, override_meta, caplog): assert "valid_device" in experiment.devices assert "invalid_device" not in experiment.devices - assert experiment.start_time == pytest.approx(0.0), ( - f"Expected start_time=0.0, got {experiment.start_time}" - ) - assert experiment.end_time == pytest.approx(10.0), ( - f"Expected end_time=10.0, got {experiment.end_time}" - ) - assert any("invalid_device" in message for message in caplog.messages),( - "Expected warning about invalid_device was skipped" - ) \ No newline at end of file + assert experiment.start_time == pytest.approx( + 0.0 + ), f"Expected start_time=0.0, got {experiment.start_time}" + assert experiment.end_time == pytest.approx( + 10.0 + ), f"Expected end_time=10.0, got {experiment.end_time}" + assert any( + "invalid_device" in message for message in caplog.messages + ), "Expected warning about invalid_device was skipped" From 863e52c991ea7c916cf2747b2c3d4044acf0791f Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:54:16 +0530 Subject: [PATCH 21/24] fix: restore imports to match main branch --- experanto/experiment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/experanto/experiment.py b/experanto/experiment.py index 897e3021..5b944e3e 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -1,7 +1,9 @@ from __future__ import annotations import logging +import re import warnings +from collections.abc import Sequence from pathlib import Path from typing import Union From acbfd6c901a4d716c192003de593c631b194353d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 19 Mar 2026 12:25:26 +0000 Subject: [PATCH 22/24] style: auto-format with black and isort --- experanto/experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experanto/experiment.py b/experanto/experiment.py index 5b944e3e..89a20f4a 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -3,7 +3,7 @@ import logging import re import warnings -from collections.abc import Sequence +from collections.abc import Sequence from pathlib import Path from typing import Union From 63f758923839972b5970f4e4d9fb8dcb82f4141c Mon Sep 17 00:00:00 2001 From: binary69 <220374295+binary69@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:51:17 +0530 Subject: [PATCH 23/24] fix: use correct sampling_rate parameter and random values in union test --- tests/create_experiment.py | 7 ++++++- tests/test_experiment.py | 40 ++++++++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/tests/create_experiment.py b/tests/create_experiment.py index 48dd124c..2a97c074 100644 --- a/tests/create_experiment.py +++ b/tests/create_experiment.py @@ -48,8 +48,13 @@ def make_sequence_device( def make_modality_config(*device_names, sampling_rates=None, offsets=None): if sampling_rates is None: sampling_rates = [10.0] * len(device_names) + elif isinstance(sampling_rates, (int, float)): + sampling_rates = [sampling_rates] * len(device_names) + if offsets is None: offsets = [0.0] * len(device_names) + elif isinstance(offsets, (int, float)): + offsets = [offsets] * len(device_names) assert len(device_names) == len( sampling_rates @@ -60,5 +65,5 @@ def make_modality_config(*device_names, sampling_rates=None, offsets=None): return { name: {"interpolation": {"sampling_rate": sr, "offset": off}} - for name, sr, off in zip(device_names, sampling_rates, offsets) + for name, sr, off in zip(device_names, sampling_rates, offsets, strict=True) } diff --git a/tests/test_experiment.py b/tests/test_experiment.py index c06e96cf..f00dfa27 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -1,6 +1,7 @@ import logging from contextlib import ExitStack +import numpy as np import pytest from experanto.experiment import Experiment @@ -76,22 +77,23 @@ def test_experiment_start_end_time_reflects_union( device_names = [f"device_{i}" for i in range(len(device_ranges))] with ExitStack() as stack: - for name, (start, end) in zip(device_names, device_ranges): + for name, (start, end) in zip(device_names, device_ranges, strict=True): stack.enter_context( make_sequence_device( - tmp_path, name, start=start, end=end, n_signals=n_signals + tmp_path, name, start=start, end=end, n_signals=n_signals, + sampling_rate=float(np.random.randint(5, 30)), ) ) experiment = Experiment( root_folder=tmp_path, - modality_config=make_modality_config(*device_names), + modality_config=make_modality_config(*device_names, offsets=[float(np.random.rand()) for _ in device_names]), ) - assert experiment.start_time == pytest.approx( + assert experiment.start_time == ( expected_start ), f"Expected start_time={expected_start}, got {experiment.start_time}" - assert experiment.end_time == pytest.approx( + assert experiment.end_time == ( expected_end ), f"Expected end_time={expected_end}, got {experiment.end_time}" @@ -149,14 +151,22 @@ def test_experiment_skips_invalid_devices(tmp_path, override_meta, caplog): device is present. The experiment time range should reflect only the valid device. """ + start_val = np.random.lognormal(mean=0.0, sigma=1.0) # Strictly positive float + duration_val = np.random.lognormal(mean=0.0, sigma=1.0) + end_val = start_val + duration_val + + start_nonval = np.random.lognormal(mean=0.0, sigma=1.0) + duration_nonval = np.random.lognormal(mean=0.0, sigma=1.0) + end_nonval = start_nonval + duration_nonval + with ExitStack() as stack: # Valid device with proper metadata stack.enter_context( make_sequence_device( tmp_path, "valid_device", - start=0.0, - end=10.0, + start=start_val, + end=end_val, ) ) # Invalid device with missing start_time and end_time @@ -164,8 +174,8 @@ def test_experiment_skips_invalid_devices(tmp_path, override_meta, caplog): make_sequence_device( tmp_path, "invalid_device", - start=0.0, - end=10.0, + start=start_nonval, + end=end_nonval, override_meta=override_meta, ) ) @@ -179,12 +189,12 @@ def test_experiment_skips_invalid_devices(tmp_path, override_meta, caplog): assert "valid_device" in experiment.devices assert "invalid_device" not in experiment.devices - assert experiment.start_time == pytest.approx( - 0.0 - ), f"Expected start_time=0.0, got {experiment.start_time}" - assert experiment.end_time == pytest.approx( - 10.0 - ), f"Expected end_time=10.0, got {experiment.end_time}" + assert experiment.start_time == ( + start_val + ), f"Expected start_time={start_val}, got {experiment.start_time}" + assert experiment.end_time == ( + end_val + ), f"Expected end_time={end_val}, got {experiment.end_time}" assert any( "invalid_device" in message for message in caplog.messages ), "Expected warning about invalid_device was skipped" From 2488615d1caa58162bdb0e24efe6dace0b9ccd83 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 19 Mar 2026 16:21:58 +0000 Subject: [PATCH 24/24] style: auto-format with black and isort --- tests/test_experiment.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/test_experiment.py b/tests/test_experiment.py index f00dfa27..72d5f2a5 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -80,14 +80,20 @@ def test_experiment_start_end_time_reflects_union( for name, (start, end) in zip(device_names, device_ranges, strict=True): stack.enter_context( make_sequence_device( - tmp_path, name, start=start, end=end, n_signals=n_signals, + tmp_path, + name, + start=start, + end=end, + n_signals=n_signals, sampling_rate=float(np.random.randint(5, 30)), ) ) experiment = Experiment( root_folder=tmp_path, - modality_config=make_modality_config(*device_names, offsets=[float(np.random.rand()) for _ in device_names]), + modality_config=make_modality_config( + *device_names, offsets=[float(np.random.rand()) for _ in device_names] + ), ) assert experiment.start_time == ( @@ -151,11 +157,11 @@ def test_experiment_skips_invalid_devices(tmp_path, override_meta, caplog): device is present. The experiment time range should reflect only the valid device. """ - start_val = np.random.lognormal(mean=0.0, sigma=1.0) # Strictly positive float - duration_val = np.random.lognormal(mean=0.0, sigma=1.0) + start_val = np.random.lognormal(mean=0.0, sigma=1.0) # Strictly positive float + duration_val = np.random.lognormal(mean=0.0, sigma=1.0) end_val = start_val + duration_val - start_nonval = np.random.lognormal(mean=0.0, sigma=1.0) + start_nonval = np.random.lognormal(mean=0.0, sigma=1.0) duration_nonval = np.random.lognormal(mean=0.0, sigma=1.0) end_nonval = start_nonval + duration_nonval