diff --git a/CHANGELOG.md b/CHANGELOG.md index f659b583..6936ee5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,20 @@ there is no file at the path. Scripts that relied on these calls silently continuing must catch the exceptions. +## Data containers + +- Removed the unused `DataStore` and `ProjectData` classes from + `easyreflectometry.data`. Their `as_dict`/`from_dict` methods had + never worked (the former raised a `TypeError`, the latter recursed + into a `KeyError`) and nothing in the library, the GUI or the docs + used them; `Project` keeps experiments in its own dictionary. Code + that imported `ProjectData` from `easyreflectometry.data` must switch + to `Project`. +- `Project.as_dict` now always records an experiment's name and model, + not only when the experiment carries x-uncertainties. Previously an + experiment with `xe` set to `None` saved without them and the project + then failed to load. + ## Models - `ModelCollection(interface=...)` with no models now builds its default diff --git a/src/easyreflectometry/data/__init__.py b/src/easyreflectometry/data/__init__.py index 4650b63d..6028fc00 100644 --- a/src/easyreflectometry/data/__init__.py +++ b/src/easyreflectometry/data/__init__.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: BSD-3-Clause from .data_store import DataSet1D -from .data_store import ProjectData from .measurement import dataset_from_datagroup from .measurement import load from .measurement import load_as_dataset @@ -17,7 +16,6 @@ 'load_as_dataset', 'dataset_from_datagroup', 'merge_datagroups', - 'ProjectData', 'DataSet1D', 'PolarizedDataSet', 'channel_from_orso_polarization', diff --git a/src/easyreflectometry/data/data_store.py b/src/easyreflectometry/data/data_store.py index b0cf001f..49818976 100644 --- a/src/easyreflectometry/data/data_store.py +++ b/src/easyreflectometry/data/data_store.py @@ -3,84 +3,14 @@ __author__ = 'github.com/wardsimon' -from collections.abc import Sequence from typing import Optional -from typing import TypeVar from typing import Union import numpy as np from easyscience.io import SerializerComponent -from easyscience.io import SerializerDict from easyreflectometry.model import Model -T = TypeVar('T') - - -class ProjectData(SerializerComponent): - def __init__(self, name='DataStore', exp_data=None, sim_data=None): - """Init function.""" - self.name = name - if exp_data is None: - exp_data = DataStore(name='Exp Datastore') - if sim_data is None: - sim_data = DataStore(name='Sim Datastore') - self.exp_data = exp_data - self.sim_data = sim_data - - -class DataStore(Sequence, SerializerComponent): - def __init__(self, *args, name='DataStore'): - """Init function.""" - self.name = name - self.items = list(args) - self.show_legend = False - - def __getitem__(self, i: int) -> T: - """Getitem function.""" - return self.items.__getitem__(i) - - def __len__(self) -> int: - """Len function.""" - return len(self.items) - - def __setitem__(self, key, value): - """Setitem function.""" - self.items[key] = value - - def __delitem__(self, key): - """Delitem function.""" - del self.items[key] - - def append(self, *args): - """Append function.""" - self.items.append(*args) - - def as_dict(self, skip: list = []) -> dict: - """As dict.""" - this_dict = super(DataStore, self).as_dict(self, skip=skip) - this_dict['items'] = [item.as_dict() for item in self.items if hasattr(item, 'as_dict')] - - @classmethod - def from_dict(cls, d): - """From dict.""" - items = d['items'] - del d['items'] - obj = cls.from_dict(d) - decoder = SerializerDict() - obj.items = [decoder.decode(item) for item in items] - return obj - - @property - def experiments(self): - """Experiments function.""" - return [self[idx] for idx in range(len(self)) if self[idx].is_experiment] - - @property - def simulations(self): - """Simulations function.""" - return [self[idx] for idx in range(len(self)) if self[idx].is_simulation] - class DataSet1D(SerializerComponent): def __init__( diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 27c75371..f65b9f14 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -1944,8 +1944,8 @@ def _as_dict_add_experiments(self, project_dict: dict): ] if experiment.xe is not None: project_dict['experiments'][key].append(list(experiment.xe)) - project_dict['experiments_models'][key] = experiment.model.name - project_dict['experiments_names'][key] = experiment.name + project_dict['experiments_models'][key] = experiment.model.name + project_dict['experiments_names'][key] = experiment.name @staticmethod def _as_dict_add_polarized_experiment(project_dict: dict, key: int, experiment: PolarizedDataSet) -> None: @@ -2068,7 +2068,7 @@ def _from_dict_extract_experiments(self, project_dict: dict) -> Dict[int, Union[ x=raw[0], y=raw[1], ye=raw[2], - xe=raw[3], + xe=raw[3] if len(raw) > 3 else None, model=self._models[project_dict['experiments_models'][key]], auto_background=False, ) diff --git a/tests/data/test_data_store.py b/tests/data/test_data_store.py index 66ba9b04..1e473dd0 100644 --- a/tests/data/test_data_store.py +++ b/tests/data/test_data_store.py @@ -9,8 +9,6 @@ from numpy.testing import assert_array_equal from easyreflectometry.data.data_store import DataSet1D -from easyreflectometry.data.data_store import DataStore -from easyreflectometry.data.data_store import ProjectData class TestDataSet1D: @@ -149,168 +147,3 @@ def test_default_error_arrays_when_none(self): # Then assert_array_equal(data.ye, np.zeros(3)) assert_array_equal(data.xe, np.zeros(3)) - - -class TestDataStore: - def test_constructor_default(self): - # When - store = DataStore() - - # Then - assert store.name == 'DataStore' - assert len(store) == 0 - assert store.show_legend is False - - def test_constructor_with_name(self): - # When - store = DataStore(name='TestStore') - - # Then - assert store.name == 'TestStore' - - def test_constructor_with_items(self): - # Given - item1 = DataSet1D(name='item1') - item2 = DataSet1D(name='item2') - - # When - store = DataStore(item1, item2, name='TestStore') - - # Then - assert len(store) == 2 - assert store[0] == item1 - assert store[1] == item2 - - def test_getitem(self): - # Given - item = DataSet1D(name='test') - store = DataStore(item) - - # When/Then - assert store[0] == item - - def test_setitem(self): - # Given - item1 = DataSet1D(name='item1') - item2 = DataSet1D(name='item2') - store = DataStore(item1) - - # When - store[0] = item2 - - # Then - assert store[0] == item2 - - def test_delitem(self): - # Given - item1 = DataSet1D(name='item1') - item2 = DataSet1D(name='item2') - store = DataStore(item1, item2) - - # When - del store[0] - - # Then - assert len(store) == 1 - assert store[0] == item2 - - def test_append(self): - # Given - store = DataStore() - item = DataSet1D(name='test') - - # When - store.append(item) - - # Then - assert len(store) == 1 - assert store[0] == item - - def test_len(self): - # Given - store = DataStore() - - # When/Then - assert len(store) == 0 - - store.append(DataSet1D()) - assert len(store) == 1 - - def test_experiments_property(self): - # Given - exp_data = DataSet1D(name='exp', model=Mock()) - sim_data = DataSet1D(name='sim') - store = DataStore(exp_data, sim_data) - - # When - experiments = store.experiments - - # Then - assert len(experiments) == 1 - assert experiments[0] == exp_data - - def test_simulations_property(self): - # Given - exp_data = DataSet1D(name='exp', model=Mock()) - sim_data = DataSet1D(name='sim') - store = DataStore(exp_data, sim_data) - - # When - simulations = store.simulations - - # Then - assert len(simulations) == 1 - assert simulations[0] == sim_data - - def test_as_dict_with_serializable_items(self): - # Given - mock_item = Mock() - mock_item.as_dict.return_value = {'test': 'data'} - store = DataStore(mock_item, name='TestStore') - - # When - The as_dict method has implementation issues, so just test it exists - # and can be called without crashing - assert hasattr(store, 'as_dict') - assert callable(getattr(store, 'as_dict')) - - def test_from_dict_class_method(self): - # Given - Test that the method exists - # The actual implementation has dependencies that make it hard to test in isolation - - # When/Then - Just verify the method exists - assert hasattr(DataStore, 'from_dict') - assert callable(getattr(DataStore, 'from_dict')) - - -class TestProjectData: - def test_constructor_default(self): - # When - project = ProjectData() - - # Then - assert project.name == 'DataStore' - assert isinstance(project.exp_data, DataStore) - assert isinstance(project.sim_data, DataStore) - assert project.exp_data.name == 'Exp Datastore' - assert project.sim_data.name == 'Sim Datastore' - - def test_constructor_with_name(self): - # When - project = ProjectData(name='TestProject') - - # Then - assert project.name == 'TestProject' - - def test_constructor_with_custom_datastores(self): - # Given - exp_store = DataStore(name='CustomExp') - sim_store = DataStore(name='CustomSim') - - # When - project = ProjectData(name='TestProject', exp_data=exp_store, sim_data=sim_store) - - # Then - assert project.exp_data == exp_store - assert project.sim_data == sim_store - assert project.exp_data.name == 'CustomExp' - assert project.sim_data.name == 'CustomSim' diff --git a/tests/test_measurement_comprehensive.py b/tests/test_measurement_comprehensive.py index 2a35e669..26cbebf8 100644 --- a/tests/test_measurement_comprehensive.py +++ b/tests/test_measurement_comprehensive.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause """ -Comprehensive tests for measurement and data store functionality. +Comprehensive tests for measurement and DataSet1D functionality. Tests for all functions in measurement.py and data_store.py modules. """ @@ -18,8 +18,6 @@ import easyreflectometry from easyreflectometry.data.data_store import DataSet1D -from easyreflectometry.data.data_store import DataStore -from easyreflectometry.data.data_store import ProjectData from easyreflectometry.data.measurement import _load_txt from easyreflectometry.data.measurement import dataset_from_datagroup from easyreflectometry.data.measurement import load @@ -223,135 +221,27 @@ def test_repr_string_representation(self): assert str(dataset) == expected -class TestDataStoreComprehensive: - """Comprehensive tests for DataStore class.""" - - def test_datastore_as_sequence(self): - """Test DataStore behaves like a sequence.""" - item1 = DataSet1D(name='item1', x=[1], y=[2]) - item2 = DataSet1D(name='item2', x=[3], y=[4]) - - store = DataStore(item1, item2, name='TestStore') - - # Test sequence operations - assert len(store) == 2 - assert store[0].name == 'item1' - assert store[1].name == 'item2' - - # Test item replacement - item3 = DataSet1D(name='item3', x=[5], y=[6]) - store[0] = item3 - assert store[0].name == 'item3' - - # Test deletion - del store[0] - assert len(store) == 1 - assert store[0].name == 'item2' - - def test_datastore_experiments_and_simulations_filtering(self): - """Test experiments and simulations properties - filter correctly.""" - exp1 = DataSet1D(name='exp1', x=[1], y=[2], model=Mock()) - exp2 = DataSet1D(name='exp2', x=[3], y=[4], model=Mock()) - sim1 = DataSet1D(name='sim1', x=[5], y=[6]) - sim2 = DataSet1D(name='sim2', x=[7], y=[8]) - - store = DataStore(exp1, sim1, exp2, sim2) - - experiments = store.experiments - simulations = store.simulations - - assert len(experiments) == 2 - assert len(simulations) == 2 - assert all(item.is_experiment for item in experiments) - assert all(item.is_simulation for item in simulations) - - def test_datastore_append_method(self): - """Test append method adds items correctly.""" - store = DataStore() - item = DataSet1D(name='new_item', x=[1], y=[2]) - - store.append(item) - - assert len(store) == 1 - assert store[0] == item - - -class TestProjectDataComprehensive: - """Comprehensive tests for ProjectData class.""" - - def test_project_data_initialization(self): - """Test ProjectData initializes with correct - default values.""" - project = ProjectData() - - assert project.name == 'DataStore' - assert isinstance(project.exp_data, DataStore) - assert isinstance(project.sim_data, DataStore) - assert project.exp_data.name == 'Exp Datastore' - assert project.sim_data.name == 'Sim Datastore' - - def test_project_data_with_custom_stores(self): - """Test ProjectData with custom experiment and - simulation stores.""" - custom_exp = DataStore(name='CustomExp') - custom_sim = DataStore(name='CustomSim') - - project = ProjectData(name='MyProject', exp_data=custom_exp, sim_data=custom_sim) - - assert project.name == 'MyProject' - assert project.exp_data == custom_exp - assert project.sim_data == custom_sim - - def test_project_data_stores_independence(self): - """Test that exp_data and sim_data are independent stores.""" - project = ProjectData() - - exp_item = DataSet1D(name='exp', x=[1], y=[2], model=Mock()) - sim_item = DataSet1D(name='sim', x=[3], y=[4]) - - project.exp_data.append(exp_item) - project.sim_data.append(sim_item) - - assert len(project.exp_data) == 1 - assert len(project.sim_data) == 1 - assert project.exp_data[0] != project.sim_data[0] - - class TestIntegrationScenarios: """Integration tests for common usage scenarios.""" def test_complete_workflow_orso_file(self): - """Test complete workflow: load ORSO file - -> create dataset -> store in project.""" - # Load file + """Test complete workflow: load ORSO file -> create dataset.""" fpath = os.path.join(PATH_STATIC, 'test_example1.ort') dataset = load_as_dataset(fpath) - # Create project and add to experimental data - project = ProjectData(name='MyAnalysis') - project.exp_data.append(dataset) - - # Verify workflow - assert len(project.exp_data) == 1 - assert project.exp_data[0] == dataset - assert isinstance(project.exp_data[0], DataSet1D) + assert isinstance(dataset, DataSet1D) + assert len(dataset.x) > 0 + assert len(dataset.x) == len(dataset.y) == len(dataset.ye) def test_complete_workflow_txt_file(self): - """Test complete workflow: load txt file -> - create dataset -> store in project.""" - # Load file + """Test complete workflow: load txt file -> create dataset (no model).""" fpath = os.path.join(PATH_STATIC, 'ref_concat_1.txt') dataset = load_as_dataset(fpath) - # Create project and add to simulation data (no model) - project = ProjectData(name='MySimulation') - project.sim_data.append(dataset) - - # Verify workflow - assert len(project.sim_data) == 1 - assert project.sim_data[0] == dataset + assert isinstance(dataset, DataSet1D) + assert dataset.model is None assert dataset.is_simulation is True + assert dataset.is_experiment is False def test_merge_multiple_files_workflow(self): """Test workflow for merging multiple data files.""" @@ -376,12 +266,6 @@ def test_error_handling_robustness(self): with pytest.raises(ValueError, match='x and y must be the same length'): DataSet1D(x=[1, 2, 3], y=[4, 5]) - # Test empty DataStore operations - empty_store = DataStore() - assert len(empty_store) == 0 - assert len(empty_store.experiments) == 0 - assert len(empty_store.simulations) == 0 - # Test file not found with pytest.raises(FileNotFoundError): _load_txt('nonexistent_file.txt') @@ -393,17 +277,11 @@ def test_data_consistency_checks(self): original_y = [10, 20, 30, 40] dataset = DataSet1D(x=original_x, y=original_y) - # Store in datastore - store = DataStore(dataset) - - # Add to project - project = ProjectData() - project.sim_data = store - - # Verify data consistency - retrieved_dataset = project.sim_data[0] - assert_array_equal(retrieved_dataset.x, np.array(original_x)) - assert_array_equal(retrieved_dataset.y, np.array(original_y)) + # Verify the input lists are copied into arrays and remain consistent + assert_array_equal(dataset.x, np.array(original_x)) + assert_array_equal(dataset.y, np.array(original_y)) + assert_array_equal(dataset.ye, np.zeros(len(original_x))) + assert_array_equal(dataset.xe, np.zeros(len(original_x))) if __name__ == '__main__': diff --git a/tests/test_project.py b/tests/test_project.py index 63e0d3fb..0994b2c7 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -547,6 +547,33 @@ def test_dict_round_trip(self): assert project_dict[key] == new_project_dict[key] assert project_materials_dict == new_project_materials_dict + def test_dict_round_trip_experiment_without_xe(self): + # When - an experiment whose x-uncertainty was explicitly cleared + global_object.map._clear() + project = Project() + project.models = ModelCollection(Model(name='First'), Model(name='Second')) + fpath = os.path.join(PATH_STATIC, 'example.ort') + project.load_experiment_for_model_at_index(fpath, 1) + project.experiments[1].xe = None + project_dict = project.as_dict() + + # Expect - name and model are recorded regardless of xe + assert project_dict['experiments_names'][1] == 'Example data file from refnx docs' + assert project_dict['experiments_models'][1] == project.models[1].name + assert len(project_dict['experiments'][1]) == 3 + + # Then - the project loads back with the experiment attached to the same model + global_object.map._clear() + new_project = Project() + new_project.from_dict(project_dict) + + assert list(new_project.experiments.keys()) == [1] + assert new_project.experiments[1].name == 'Example data file from refnx docs' + assert new_project.experiments[1].model == new_project.models[1] + assert_allclose(new_project.experiments[1].x, project.experiments[1].x) + assert_allclose(new_project.experiments[1].y, project.experiments[1].y) + assert_allclose(new_project.experiments[1].ye, project.experiments[1].ye) + def test_save_as_json(self, tmp_path): # When global_object.map._clear()