From 7309aafb6fa080c5ab7af26e17215bd3df2105fa Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 10 Feb 2026 13:26:57 +0100 Subject: [PATCH 01/15] Extend DREAM scipp-analysis integration tests --- .../scipp-analysis/dream/conftest.py | 43 ++++ .../dream/test_analyze_reduced_data.py | 202 ++++++++++++++++++ .../dream/test_package_import.py | 52 +++++ .../dream/test_read_reduced_data.py | 23 ++ .../dream/test_validate_meta_data.py | 73 +++++++ .../dream/test_validate_physical_data.py | 89 ++++++++ 6 files changed, 482 insertions(+) create mode 100644 tests/integration/scipp-analysis/dream/conftest.py create mode 100644 tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py create mode 100644 tests/integration/scipp-analysis/dream/test_package_import.py create mode 100644 tests/integration/scipp-analysis/dream/test_read_reduced_data.py create mode 100644 tests/integration/scipp-analysis/dream/test_validate_meta_data.py create mode 100644 tests/integration/scipp-analysis/dream/test_validate_physical_data.py diff --git a/tests/integration/scipp-analysis/dream/conftest.py b/tests/integration/scipp-analysis/dream/conftest.py new file mode 100644 index 00000000..55b87038 --- /dev/null +++ b/tests/integration/scipp-analysis/dream/conftest.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 DMSC +"""Shared fixtures for DREAM scipp-analysis integration tests.""" + +from pathlib import Path + +import gemmi +import pytest +from pooch import retrieve + +# CIF file URL and expected data block name +CIF_URL = 'https://pub-6c25ef91903d4301a3338bd53b370098.r2.dev/dream_reduced.cif' +DATABLOCK_NAME = 'reduced_tof' + + +@pytest.fixture(scope='module') +def cif_path() -> str: + """Retrieve the CIF file and return its path.""" + return retrieve(url=CIF_URL, known_hash=None) + + +@pytest.fixture(scope='module') +def cif_content(cif_path: str) -> str: + """Read the CIF file content as text.""" + return Path(cif_path).read_text() + + +@pytest.fixture(scope='module') +def cif_document(cif_path: str) -> gemmi.cif.Document: + """Read the CIF file with gemmi and return the document.""" + return gemmi.cif.read(cif_path) + + +@pytest.fixture(scope='module') +def cif_block(cif_document: gemmi.cif.Document) -> gemmi.cif.Block: + """Return the expected data block from the CIF document.""" + return cif_document.find_block(DATABLOCK_NAME) + + +@pytest.fixture(scope='module') +def data_loop(cif_block: gemmi.cif.Block) -> gemmi.cif.Loop: + """Return the main data loop containing point_id, tof, and intensity data.""" + return cif_block.find(['_pd_data.point_id']).loop diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py new file mode 100644 index 00000000..94c6c70c --- /dev/null +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -0,0 +1,202 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 DMSC +"""Tests for analyzing reduced diffraction data using easydiffraction. + +Based on the ed-15.py tutorial workflow for TOF powder diffraction analysis. +""" + +from pathlib import Path +from typing import Any + +import pytest + +import easydiffraction as ed +from easydiffraction import Project +from easydiffraction import SampleModelFactory + +# Experiment type tags required for easydiffraction +EXPT_TYPE_TAGS = { + '_expt_type.sample_form': 'powder', + '_expt_type.beam_mode': 'time-of-flight', + '_expt_type.radiation_probe': 'neutron', + '_expt_type.scattering_type': 'bragg', +} + + +@pytest.fixture(scope='module') +def sample_model() -> Any: + """Create a Silicon sample model for fitting.""" + model = SampleModelFactory.create(name='si') + model.space_group.name_h_m = 'F d -3 m' + model.space_group.it_coordinate_system_code = '1' + model.cell.length_a = 5.43146 + model.atom_sites.add( + label='Si', + type_symbol='Si', + fract_x=0.125, + fract_y=0.125, + fract_z=0.125, + wyckoff_letter='c', + b_iso=1.96, + ) + return model + + +@pytest.fixture(scope='module') +def prepared_cif_path(cif_path: str, tmp_path_factory: pytest.TempPathFactory) -> str: + """Prepare CIF file with experiment type tags for easydiffraction.""" + with Path(cif_path).open() as f: + content = f.read() + + # Replace zero ESDs with finite values (required for fitting) + content = content.replace(' 0.0 0.0', ' 0.0 1.0') + + # Add experiment type tags if missing + for tag, value in EXPT_TYPE_TAGS.items(): + if tag not in content: + content += f'\n{tag} {value}' + + # Write to temp file + tmp_dir = tmp_path_factory.mktemp('dream_data') + prepared_path = tmp_dir / 'dream_reduced_prepared.cif' + prepared_path.write_text(content) + + return str(prepared_path) + + +@pytest.fixture(scope='module') +def project_with_data(sample_model: Any, prepared_cif_path: str) -> Project: + """Create project with sample model and loaded experiment data.""" + project = ed.Project() + project.sample_models.add(sample_model=sample_model) + project.experiments.add(cif_path=prepared_cif_path) + return project + + +@pytest.fixture(scope='module') +def configured_project(project_with_data: Project) -> Project: + """Configure project with instrument and peak parameters for fitting.""" + project = project_with_data + experiment = project.experiments['reduced_tof'] + + # Link phase + experiment.linked_phases.add(id='si', scale=1.0) + + # Instrument setup + experiment.instrument.setup_twotheta_bank = 90.0 + experiment.instrument.calib_d_to_tof_linear = 18613.498 + + # Peak profile parameters + experiment.peak.broad_gauss_sigma_0 = 48649.0 + experiment.peak.broad_gauss_sigma_1 = 2900.0 + experiment.peak.broad_gauss_sigma_2 = 0.0 + experiment.peak.broad_mix_beta_0 = 0.0147 + experiment.peak.broad_mix_beta_1 = 0.0 + experiment.peak.asym_alpha_0 = 0.0 + experiment.peak.asym_alpha_1 = 0.26 + + # Excluded regions + experiment.excluded_regions.add(id='1', start=0, end=10000) + experiment.excluded_regions.add(id='2', start=70000, end=200000) + + # Background points + background_points = [ + ('2', 10000, 0.0), + ('3', 14000, 0.26), + ('4', 21000, 0.5), + ('5', 27500, 0.55), + ('6', 40000, 0.31), + ('7', 50000, 0.6), + ('8', 61000, 0.53), + ('9', 70000, 0.58), + ] + for id_, x, y in background_points: + experiment.background.add(id=id_, x=x, y=y) + + return project + + +@pytest.fixture(scope='module') +def fit_results(configured_project: Project) -> dict[str, Any]: + """Perform fit and return results.""" + project = configured_project + model = project.sample_models['si'] + experiment = project.experiments['reduced_tof'] + + # Set free parameters for background + for point in experiment.background: + point.y.free = True + + # Set free parameters for fitting + model.atom_sites['Si'].b_iso.free = True + experiment.linked_phases['si'].scale.free = True + experiment.instrument.calib_d_to_tof_linear.free = True + experiment.peak.broad_gauss_sigma_0.free = True + experiment.peak.broad_gauss_sigma_1.free = True + experiment.peak.broad_mix_beta_0.free = True + experiment.peak.asym_alpha_1.free = True + + # Run fit + project.analysis.fit() + + return { + 'success': project.analysis.fit_results.success, + 'reduced_chi': project.analysis.fit_results.reduced_chi_square, + 'n_free_params': len(project.analysis.fittable_params), + } + + +# ============================================================================= +# Test: Data Loading +# ============================================================================= + + +def test_analyze_data__load_cif(project_with_data: Project) -> None: + """Verify CIF data loads into project correctly.""" + assert 'reduced_tof' in project_with_data.experiments.names + + +def test_analyze_data__data_size(project_with_data: Project) -> None: + """Verify loaded data has expected size.""" + experiment = project_with_data.experiments['reduced_tof'] + # Data should have substantial number of points + assert experiment.data.x.size > 100 + + +# ============================================================================= +# Test: Configuration +# ============================================================================= + + +def test_analyze_data__phase_linked(configured_project: Project) -> None: + """Verify phase is correctly linked to experiment.""" + experiment = configured_project.experiments['reduced_tof'] + # Extract actual id values from linked_phases + phase_ids = [ + p.id.value if hasattr(p.id, 'value') else str(p.id) for p in experiment.linked_phases + ] + assert 'si' in phase_ids + + +def test_analyze_data__background_set(configured_project: Project) -> None: + """Verify background points are configured.""" + experiment = configured_project.experiments['reduced_tof'] + assert len(list(experiment.background)) >= 5 + + +# ============================================================================= +# Test: Fitting +# ============================================================================= + + +@pytest.mark.skip(reason='Fitting not yet working with reduced DREAM data') +def test_analyze_data__fit_success(fit_results: dict[str, Any]) -> None: + """Verify fitting completes successfully.""" + assert fit_results['success'] is True + + +@pytest.mark.skip(reason='Fitting not yet working with reduced DREAM data') +def test_analyze_data__fit_quality(fit_results: dict[str, Any]) -> None: + """Verify fit quality is reasonable (chi-square < threshold).""" + # Reduced chi-square should be reasonable for a good fit + assert fit_results['reduced_chi'] < 10.0 diff --git a/tests/integration/scipp-analysis/dream/test_package_import.py b/tests/integration/scipp-analysis/dream/test_package_import.py new file mode 100644 index 00000000..ef951c65 --- /dev/null +++ b/tests/integration/scipp-analysis/dream/test_package_import.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 DMSC +"""Tests for verifying package installation and importability.""" + +import importlib.metadata + +import pytest +import requests +from packaging.version import Version + +PACKAGE_NAMES = ['easydiffraction', 'essdiffraction'] +PYPI_URL = 'https://pypi.org/pypi/{}/json' + + +def get_installed_version(package_name: str) -> str | None: + """Get the installed version of a package.""" + try: + return importlib.metadata.version(package_name) + except importlib.metadata.PackageNotFoundError: + return None + + +def get_latest_version(package_name: str) -> str | None: + """Get the latest version of a package from PyPI.""" + response = requests.get(PYPI_URL.format(package_name), timeout=10) + if response.status_code == 200: + return response.json()['info']['version'] + return None + + +def get_base_version(version_str: str) -> str: + """Extract MAJOR.MINOR.PATCH from version string, ignoring local identifiers.""" + v = Version(version_str) + return v.base_version + + +@pytest.mark.parametrize('package_name', PACKAGE_NAMES) +def test_package_import__latest(package_name: str) -> None: + """Verify installed package matches PyPI latest version (MAJOR.MINOR.PATCH).""" + installed_version = get_installed_version(package_name) + latest_version = get_latest_version(package_name) + + assert installed_version is not None, f'Package {package_name} is not installed.' + assert latest_version is not None, f'Could not fetch latest version for {package_name}.' + + # Compare only MAJOR.MINOR.PATCH, ignoring local version identifiers + installed_base = get_base_version(installed_version) + latest_base = get_base_version(latest_version) + + assert installed_base == latest_base, ( + f'Package {package_name} is outdated: Installed={installed_base}, Latest={latest_base}' + ) diff --git a/tests/integration/scipp-analysis/dream/test_read_reduced_data.py b/tests/integration/scipp-analysis/dream/test_read_reduced_data.py new file mode 100644 index 00000000..fa498b4a --- /dev/null +++ b/tests/integration/scipp-analysis/dream/test_read_reduced_data.py @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 DMSC +"""Tests for reading reduced data from CIF files.""" + +import gemmi + + +def test_read_reduced_data__fetch_cif(cif_path: str) -> None: + """Verify that the CIF file can be fetched from remote URL.""" + assert cif_path is not None + assert len(cif_path) > 0 + + +def test_read_reduced_data__py_read_cif(cif_content: str) -> None: + """Verify that the CIF file can be read as text and has correct format.""" + assert len(cif_content) > 0 # Check file is not empty + assert '#\\#CIF_1.1' in cif_content # Check CIF version is 1.1 + + +def test_read_reduced_data__gemmi_parse(cif_document: gemmi.cif.Document) -> None: + """Verify that gemmi can parse the CIF document.""" + assert cif_document is not None + assert len(cif_document) > 0 diff --git a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py new file mode 100644 index 00000000..841709de --- /dev/null +++ b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 DMSC +"""Tests for validating metadata structure in CIF files.""" + +import gemmi +import pytest + + +def test_validate_meta_data__block_exists( + cif_document: gemmi.cif.Document, +) -> None: + """Verify that single datablock 'reduced_tof' is present.""" + assert len(cif_document) == 1 + assert cif_document[0].name == 'reduced_tof' + + +def test_validate_meta_data__diffrn_radiation( + cif_block: gemmi.cif.Block, +) -> None: + """Verify _diffrn_radiation.probe is 'neutron'.""" + probe = cif_block.find_value('_diffrn_radiation.probe') + assert probe is not None + assert str(probe).strip('\'"') == 'neutron' + + +def test_validate_meta_data__d_to_tof_loop( + cif_block: gemmi.cif.Block, +) -> None: + """Verify the d_to_tof calibration loop exists with correct structure.""" + loop = cif_block.find(['_pd_calib_d_to_tof.id']).loop + assert loop is not None + + # Check all expected columns exist + tags = [tag for tag in loop.tags] + assert '_pd_calib_d_to_tof.id' in tags + assert '_pd_calib_d_to_tof.power' in tags + assert '_pd_calib_d_to_tof.coeff' in tags + + +def test_validate_meta_data__d_to_tof_difc( + cif_block: gemmi.cif.Block, +) -> None: + """Verify DIFC calibration coefficient is approximately 9819.35.""" + table = cif_block.find([ + '_pd_calib_d_to_tof.id', + '_pd_calib_d_to_tof.power', + '_pd_calib_d_to_tof.coeff', + ]) + + difc_row = None + for row in table: + if row[0] == 'DIFC': + difc_row = row + break + + assert difc_row is not None, 'DIFC row not found in calibration loop' + assert int(difc_row[1]) == 1, 'DIFC power should be 1' + assert pytest.approx(float(difc_row[2]), rel=0.01) == 9819.35 + + +def test_validate_meta_data__data_loop_exists( + cif_block: gemmi.cif.Block, +) -> None: + """Verify the main data loop exists with required columns.""" + loop = cif_block.find(['_pd_data.point_id']).loop + assert loop is not None + + # Check all expected columns exist + tags = [tag for tag in loop.tags] + assert '_pd_data.point_id' in tags + assert '_pd_meas.time_of_flight' in tags + assert '_pd_proc.intensity_norm' in tags + assert '_pd_proc.intensity_norm_su' in tags diff --git a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py new file mode 100644 index 00000000..bbf522b7 --- /dev/null +++ b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 DMSC +"""Tests for validating physical data values in CIF files.""" + +import gemmi +import numpy as np +import pytest + +# Expected number of data points in the loop +LOOP_SIZE = 200 + + +def _get_column_values(cif_block: gemmi.cif.Block, tag: str) -> np.ndarray: + """Helper to extract column values as numpy array.""" + column = cif_block.find([tag]) + return np.array([float(row[0]) for row in column]) + + +def test_validate_phys_data__data_size( + cif_block: gemmi.cif.Block, +) -> None: + """Verify the data loop contains exactly 2000 points.""" + loop = cif_block.find(['_pd_data.point_id']).loop + assert loop.length() == LOOP_SIZE + + +def test_validate_phys_data__point_id_type( + cif_block: gemmi.cif.Block, +) -> None: + """Verify _pd_data.point_id contains integers from 0 to 1999.""" + point_ids = _get_column_values(cif_block, '_pd_data.point_id').astype(int) + + assert len(point_ids) == LOOP_SIZE + assert point_ids[0] == 0 + assert point_ids[-1] == LOOP_SIZE - 1 + # Verify sequential integers + np.testing.assert_array_equal(point_ids, np.arange(LOOP_SIZE)) + + +def test_validate_phys_data__tof_positive( + cif_block: gemmi.cif.Block, +) -> None: + """Verify _pd_meas.time_of_flight values are positive floats.""" + tof_values = _get_column_values(cif_block, '_pd_meas.time_of_flight') + + assert np.all(tof_values > 0), 'TOF values must be positive' + + +def test_validate_phys_data__tof_increasing( + cif_block: gemmi.cif.Block, +) -> None: + """Verify _pd_meas.time_of_flight values constantly increase.""" + tof_values = _get_column_values(cif_block, '_pd_meas.time_of_flight') + + assert np.all(np.diff(tof_values) > 0), 'TOF values must be strictly increasing' + + +def test_validate_phys_data__tof_range( + cif_block: gemmi.cif.Block, +) -> None: + """Verify TOF range: first ~57.53, last ~22953.14.""" + tof_values = _get_column_values(cif_block, '_pd_meas.time_of_flight') + + assert pytest.approx(tof_values[0], rel=0.01) == 57.53 + assert pytest.approx(tof_values[-1], rel=0.01) == 22953.14 + + +def test_validate_phys_data__intensity_range( + cif_block: gemmi.cif.Block, +) -> None: + """Verify _pd_proc.intensity_norm is non-negative.""" + intensity = _get_column_values(cif_block, '_pd_proc.intensity_norm') + + assert np.all(intensity >= 0), 'Intensity values must be non-negative' + # First and last values in actual file are 0.0 + assert intensity[0] == pytest.approx(0.0, abs=0.01) + assert intensity[-1] == pytest.approx(0.0, abs=0.01) + + +def test_validate_phys_data__intensity_su( + cif_block: gemmi.cif.Block, +) -> None: + """Verify _pd_proc.intensity_norm_su is non-negative.""" + intensity_su = _get_column_values(cif_block, '_pd_proc.intensity_norm_su') + + assert np.all(intensity_su >= 0), 'Intensity SU values must be non-negative' + # First and last values in actual file are 0.0 + assert intensity_su[0] == pytest.approx(0.0, abs=0.01) + assert intensity_su[-1] == pytest.approx(0.0, abs=0.01) From f8e309dcb2b13e98df2f9d34878f5c1324147375 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 10 Feb 2026 14:15:22 +0100 Subject: [PATCH 02/15] Fetch CIF uncached and update DREAM data checks --- .../scipp-analysis/dream/conftest.py | 11 +++++---- .../dream/test_analyze_reduced_data.py | 23 +++++++++++-------- .../dream/test_package_import.py | 8 +++++-- .../dream/test_read_reduced_data.py | 4 +++- .../dream/test_validate_meta_data.py | 8 ++++--- .../dream/test_validate_physical_data.py | 22 ++++++++++-------- 6 files changed, 47 insertions(+), 29 deletions(-) diff --git a/tests/integration/scipp-analysis/dream/conftest.py b/tests/integration/scipp-analysis/dream/conftest.py index 55b87038..7204eeda 100644 --- a/tests/integration/scipp-analysis/dream/conftest.py +++ b/tests/integration/scipp-analysis/dream/conftest.py @@ -14,9 +14,10 @@ @pytest.fixture(scope='module') -def cif_path() -> str: - """Retrieve the CIF file and return its path.""" - return retrieve(url=CIF_URL, known_hash=None) +def cif_path(tmp_path_factory: pytest.TempPathFactory) -> str: + """Download CIF file fresh (no caching) and return its path.""" + tmp_dir = tmp_path_factory.mktemp('dream_data') + return retrieve(url=CIF_URL, known_hash=None, path=tmp_dir) @pytest.fixture(scope='module') @@ -39,5 +40,7 @@ def cif_block(cif_document: gemmi.cif.Document) -> gemmi.cif.Block: @pytest.fixture(scope='module') def data_loop(cif_block: gemmi.cif.Block) -> gemmi.cif.Loop: - """Return the main data loop containing point_id, tof, and intensity data.""" + """Return the main data loop containing point_id, tof, and intensity + data. + """ return cif_block.find(['_pd_data.point_id']).loop diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py index 94c6c70c..bb315e4e 100644 --- a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -2,7 +2,8 @@ # Copyright (c) 2026 DMSC """Tests for analyzing reduced diffraction data using easydiffraction. -Based on the ed-15.py tutorial workflow for TOF powder diffraction analysis. +Based on the ed-15.py tutorial workflow for TOF powder diffraction +analysis. """ from pathlib import Path @@ -44,7 +45,9 @@ def sample_model() -> Any: @pytest.fixture(scope='module') def prepared_cif_path(cif_path: str, tmp_path_factory: pytest.TempPathFactory) -> str: - """Prepare CIF file with experiment type tags for easydiffraction.""" + """Prepare CIF file with experiment type tags for + easydiffraction. + """ with Path(cif_path).open() as f: content = f.read() @@ -75,7 +78,9 @@ def project_with_data(sample_model: Any, prepared_cif_path: str) -> Project: @pytest.fixture(scope='module') def configured_project(project_with_data: Project) -> Project: - """Configure project with instrument and peak parameters for fitting.""" + """Configure project with instrument and peak parameters for + fitting. + """ project = project_with_data experiment = project.experiments['reduced_tof'] @@ -146,9 +151,9 @@ def fit_results(configured_project: Project) -> dict[str, Any]: } -# ============================================================================= +# ====================================================================== # Test: Data Loading -# ============================================================================= +# ====================================================================== def test_analyze_data__load_cif(project_with_data: Project) -> None: @@ -163,9 +168,9 @@ def test_analyze_data__data_size(project_with_data: Project) -> None: assert experiment.data.x.size > 100 -# ============================================================================= +# ====================================================================== # Test: Configuration -# ============================================================================= +# ====================================================================== def test_analyze_data__phase_linked(configured_project: Project) -> None: @@ -184,9 +189,9 @@ def test_analyze_data__background_set(configured_project: Project) -> None: assert len(list(experiment.background)) >= 5 -# ============================================================================= +# ====================================================================== # Test: Fitting -# ============================================================================= +# ====================================================================== @pytest.mark.skip(reason='Fitting not yet working with reduced DREAM data') diff --git a/tests/integration/scipp-analysis/dream/test_package_import.py b/tests/integration/scipp-analysis/dream/test_package_import.py index ef951c65..e9fc44d1 100644 --- a/tests/integration/scipp-analysis/dream/test_package_import.py +++ b/tests/integration/scipp-analysis/dream/test_package_import.py @@ -29,14 +29,18 @@ def get_latest_version(package_name: str) -> str | None: def get_base_version(version_str: str) -> str: - """Extract MAJOR.MINOR.PATCH from version string, ignoring local identifiers.""" + """Extract MAJOR.MINOR.PATCH from version string, ignoring local + identifiers. + """ v = Version(version_str) return v.base_version @pytest.mark.parametrize('package_name', PACKAGE_NAMES) def test_package_import__latest(package_name: str) -> None: - """Verify installed package matches PyPI latest version (MAJOR.MINOR.PATCH).""" + """Verify installed package matches PyPI latest version + (MAJOR.MINOR.PATCH). + """ installed_version = get_installed_version(package_name) latest_version = get_latest_version(package_name) diff --git a/tests/integration/scipp-analysis/dream/test_read_reduced_data.py b/tests/integration/scipp-analysis/dream/test_read_reduced_data.py index fa498b4a..32555332 100644 --- a/tests/integration/scipp-analysis/dream/test_read_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_read_reduced_data.py @@ -12,7 +12,9 @@ def test_read_reduced_data__fetch_cif(cif_path: str) -> None: def test_read_reduced_data__py_read_cif(cif_content: str) -> None: - """Verify that the CIF file can be read as text and has correct format.""" + """Verify that the CIF file can be read as text and has correct + format. + """ assert len(cif_content) > 0 # Check file is not empty assert '#\\#CIF_1.1' in cif_content # Check CIF version is 1.1 diff --git a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py index 841709de..259fdddc 100644 --- a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py +++ b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py @@ -26,7 +26,9 @@ def test_validate_meta_data__diffrn_radiation( def test_validate_meta_data__d_to_tof_loop( cif_block: gemmi.cif.Block, ) -> None: - """Verify the d_to_tof calibration loop exists with correct structure.""" + """Verify the d_to_tof calibration loop exists with correct + structure. + """ loop = cif_block.find(['_pd_calib_d_to_tof.id']).loop assert loop is not None @@ -40,7 +42,7 @@ def test_validate_meta_data__d_to_tof_loop( def test_validate_meta_data__d_to_tof_difc( cif_block: gemmi.cif.Block, ) -> None: - """Verify DIFC calibration coefficient is approximately 9819.35.""" + """Verify DIFC calibration coefficient is approximately 28385.3.""" table = cif_block.find([ '_pd_calib_d_to_tof.id', '_pd_calib_d_to_tof.power', @@ -55,7 +57,7 @@ def test_validate_meta_data__d_to_tof_difc( assert difc_row is not None, 'DIFC row not found in calibration loop' assert int(difc_row[1]) == 1, 'DIFC power should be 1' - assert pytest.approx(float(difc_row[2]), rel=0.01) == 9819.35 + assert pytest.approx(float(difc_row[2]), rel=0.01) == 28385.3 def test_validate_meta_data__data_loop_exists( diff --git a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py index bbf522b7..bfdd8978 100644 --- a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py +++ b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py @@ -7,7 +7,7 @@ import pytest # Expected number of data points in the loop -LOOP_SIZE = 200 +LOOP_SIZE = 2000 def _get_column_values(cif_block: gemmi.cif.Block, tag: str) -> np.ndarray: @@ -58,32 +58,34 @@ def test_validate_phys_data__tof_increasing( def test_validate_phys_data__tof_range( cif_block: gemmi.cif.Block, ) -> None: - """Verify TOF range: first ~57.53, last ~22953.14.""" + """Verify TOF range: first ~8530.1, last ~66503.7.""" tof_values = _get_column_values(cif_block, '_pd_meas.time_of_flight') - assert pytest.approx(tof_values[0], rel=0.01) == 57.53 - assert pytest.approx(tof_values[-1], rel=0.01) == 22953.14 + assert pytest.approx(tof_values[0], rel=0.01) == 8530.1 + assert pytest.approx(tof_values[-1], rel=0.01) == 66503.7 def test_validate_phys_data__intensity_range( cif_block: gemmi.cif.Block, ) -> None: - """Verify _pd_proc.intensity_norm is non-negative.""" + """Verify _pd_proc.intensity_norm is non-negative with expected + bounds. + """ intensity = _get_column_values(cif_block, '_pd_proc.intensity_norm') assert np.all(intensity >= 0), 'Intensity values must be non-negative' - # First and last values in actual file are 0.0 assert intensity[0] == pytest.approx(0.0, abs=0.01) - assert intensity[-1] == pytest.approx(0.0, abs=0.01) + assert intensity[-1] == pytest.approx(0.68, rel=0.1) def test_validate_phys_data__intensity_su( cif_block: gemmi.cif.Block, ) -> None: - """Verify _pd_proc.intensity_norm_su is non-negative.""" + """Verify _pd_proc.intensity_norm_su is non-negative with expected + bounds. + """ intensity_su = _get_column_values(cif_block, '_pd_proc.intensity_norm_su') assert np.all(intensity_su >= 0), 'Intensity SU values must be non-negative' - # First and last values in actual file are 0.0 assert intensity_su[0] == pytest.approx(0.0, abs=0.01) - assert intensity_su[-1] == pytest.approx(0.0, abs=0.01) + assert intensity_su[-1] == pytest.approx(0.04, rel=0.1) From d7a4f8fc4dea290af2af5e4619c551a2aca127cb Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 10 Feb 2026 14:22:47 +0100 Subject: [PATCH 03/15] Refine DREAM integration tests for robustness --- .../scipp-analysis/dream/conftest.py | 20 +++++++--- .../dream/test_analyze_reduced_data.py | 6 +-- .../dream/test_package_import.py | 16 ++++++-- .../dream/test_read_reduced_data.py | 12 ++++-- .../dream/test_validate_meta_data.py | 39 +++++++++---------- .../dream/test_validate_physical_data.py | 39 +++++++++---------- 6 files changed, 76 insertions(+), 56 deletions(-) diff --git a/tests/integration/scipp-analysis/dream/conftest.py b/tests/integration/scipp-analysis/dream/conftest.py index 7204eeda..c53b308f 100644 --- a/tests/integration/scipp-analysis/dream/conftest.py +++ b/tests/integration/scipp-analysis/dream/conftest.py @@ -14,32 +14,42 @@ @pytest.fixture(scope='module') -def cif_path(tmp_path_factory: pytest.TempPathFactory) -> str: +def cif_path( + tmp_path_factory: pytest.TempPathFactory, +) -> str: """Download CIF file fresh (no caching) and return its path.""" tmp_dir = tmp_path_factory.mktemp('dream_data') return retrieve(url=CIF_URL, known_hash=None, path=tmp_dir) @pytest.fixture(scope='module') -def cif_content(cif_path: str) -> str: +def cif_content( + cif_path: str, +) -> str: """Read the CIF file content as text.""" return Path(cif_path).read_text() @pytest.fixture(scope='module') -def cif_document(cif_path: str) -> gemmi.cif.Document: +def cif_document( + cif_path: str, +) -> gemmi.cif.Document: """Read the CIF file with gemmi and return the document.""" return gemmi.cif.read(cif_path) @pytest.fixture(scope='module') -def cif_block(cif_document: gemmi.cif.Document) -> gemmi.cif.Block: +def cif_block( + cif_document: gemmi.cif.Document, +) -> gemmi.cif.Block: """Return the expected data block from the CIF document.""" return cif_document.find_block(DATABLOCK_NAME) @pytest.fixture(scope='module') -def data_loop(cif_block: gemmi.cif.Block) -> gemmi.cif.Loop: +def data_loop( + cif_block: gemmi.cif.Block, +) -> gemmi.cif.Loop: """Return the main data loop containing point_id, tof, and intensity data. """ diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py index bb315e4e..e099652b 100644 --- a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -1,9 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2026 DMSC -"""Tests for analyzing reduced diffraction data using easydiffraction. - -Based on the ed-15.py tutorial workflow for TOF powder diffraction -analysis. +"""Tests for analyzing reduced diffraction data using +easydiffraction. """ from pathlib import Path diff --git a/tests/integration/scipp-analysis/dream/test_package_import.py b/tests/integration/scipp-analysis/dream/test_package_import.py index e9fc44d1..b5223910 100644 --- a/tests/integration/scipp-analysis/dream/test_package_import.py +++ b/tests/integration/scipp-analysis/dream/test_package_import.py @@ -12,7 +12,9 @@ PYPI_URL = 'https://pypi.org/pypi/{}/json' -def get_installed_version(package_name: str) -> str | None: +def get_installed_version( + package_name: str, +) -> str | None: """Get the installed version of a package.""" try: return importlib.metadata.version(package_name) @@ -20,7 +22,9 @@ def get_installed_version(package_name: str) -> str | None: return None -def get_latest_version(package_name: str) -> str | None: +def get_latest_version( + package_name: str, +) -> str | None: """Get the latest version of a package from PyPI.""" response = requests.get(PYPI_URL.format(package_name), timeout=10) if response.status_code == 200: @@ -28,7 +32,9 @@ def get_latest_version(package_name: str) -> str | None: return None -def get_base_version(version_str: str) -> str: +def get_base_version( + version_str: str, +) -> str: """Extract MAJOR.MINOR.PATCH from version string, ignoring local identifiers. """ @@ -37,7 +43,9 @@ def get_base_version(version_str: str) -> str: @pytest.mark.parametrize('package_name', PACKAGE_NAMES) -def test_package_import__latest(package_name: str) -> None: +def test_package_import__latest( + package_name: str, +) -> None: """Verify installed package matches PyPI latest version (MAJOR.MINOR.PATCH). """ diff --git a/tests/integration/scipp-analysis/dream/test_read_reduced_data.py b/tests/integration/scipp-analysis/dream/test_read_reduced_data.py index 32555332..51f26d99 100644 --- a/tests/integration/scipp-analysis/dream/test_read_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_read_reduced_data.py @@ -5,13 +5,17 @@ import gemmi -def test_read_reduced_data__fetch_cif(cif_path: str) -> None: +def test_read_reduced_data__fetch_cif( + cif_path: str, +) -> None: """Verify that the CIF file can be fetched from remote URL.""" assert cif_path is not None assert len(cif_path) > 0 -def test_read_reduced_data__py_read_cif(cif_content: str) -> None: +def test_read_reduced_data__py_read_cif( + cif_content: str, +) -> None: """Verify that the CIF file can be read as text and has correct format. """ @@ -19,7 +23,9 @@ def test_read_reduced_data__py_read_cif(cif_content: str) -> None: assert '#\\#CIF_1.1' in cif_content # Check CIF version is 1.1 -def test_read_reduced_data__gemmi_parse(cif_document: gemmi.cif.Document) -> None: +def test_read_reduced_data__gemmi_parse_cif( + cif_document: gemmi.cif.Document, +) -> None: """Verify that gemmi can parse the CIF document.""" assert cif_document is not None assert len(cif_document) > 0 diff --git a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py index 259fdddc..403faa33 100644 --- a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py +++ b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py @@ -11,7 +11,7 @@ def test_validate_meta_data__block_exists( ) -> None: """Verify that single datablock 'reduced_tof' is present.""" assert len(cif_document) == 1 - assert cif_document[0].name == 'reduced_tof' + assert cif_document.sole_block().name == 'reduced_tof' def test_validate_meta_data__diffrn_radiation( @@ -20,7 +20,8 @@ def test_validate_meta_data__diffrn_radiation( """Verify _diffrn_radiation.probe is 'neutron'.""" probe = cif_block.find_value('_diffrn_radiation.probe') assert probe is not None - assert str(probe).strip('\'"') == 'neutron' + assert isinstance(probe, str) + assert probe == 'neutron' def test_validate_meta_data__d_to_tof_loop( @@ -32,11 +33,13 @@ def test_validate_meta_data__d_to_tof_loop( loop = cif_block.find(['_pd_calib_d_to_tof.id']).loop assert loop is not None + # Check loop has exactly 1 row + assert loop.length() == 1 + # Check all expected columns exist - tags = [tag for tag in loop.tags] - assert '_pd_calib_d_to_tof.id' in tags - assert '_pd_calib_d_to_tof.power' in tags - assert '_pd_calib_d_to_tof.coeff' in tags + assert '_pd_calib_d_to_tof.id' in loop.tags + assert '_pd_calib_d_to_tof.power' in loop.tags + assert '_pd_calib_d_to_tof.coeff' in loop.tags def test_validate_meta_data__d_to_tof_difc( @@ -48,16 +51,13 @@ def test_validate_meta_data__d_to_tof_difc( '_pd_calib_d_to_tof.power', '_pd_calib_d_to_tof.coeff', ]) + loop = table.loop + assert loop is not None - difc_row = None - for row in table: - if row[0] == 'DIFC': - difc_row = row - break - - assert difc_row is not None, 'DIFC row not found in calibration loop' - assert int(difc_row[1]) == 1, 'DIFC power should be 1' - assert pytest.approx(float(difc_row[2]), rel=0.01) == 28385.3 + id, power, coeff = loop.values + assert id == 'DIFC' + assert int(power) == 1 + assert pytest.approx(float(coeff), rel=0.01) == 28385.3 def test_validate_meta_data__data_loop_exists( @@ -68,8 +68,7 @@ def test_validate_meta_data__data_loop_exists( assert loop is not None # Check all expected columns exist - tags = [tag for tag in loop.tags] - assert '_pd_data.point_id' in tags - assert '_pd_meas.time_of_flight' in tags - assert '_pd_proc.intensity_norm' in tags - assert '_pd_proc.intensity_norm_su' in tags + assert '_pd_data.point_id' in loop.tags + assert '_pd_meas.time_of_flight' in loop.tags + assert '_pd_proc.intensity_norm' in loop.tags + assert '_pd_proc.intensity_norm_su' in loop.tags diff --git a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py index bfdd8978..2bb51f1b 100644 --- a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py +++ b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py @@ -10,13 +10,16 @@ LOOP_SIZE = 2000 -def _get_column_values(cif_block: gemmi.cif.Block, tag: str) -> np.ndarray: +def get_column_values( + cif_block: gemmi.cif.Block, + tag: str, +) -> np.ndarray: """Helper to extract column values as numpy array.""" column = cif_block.find([tag]) return np.array([float(row[0]) for row in column]) -def test_validate_phys_data__data_size( +def test_validate_physical_data__data_size( cif_block: gemmi.cif.Block, ) -> None: """Verify the data loop contains exactly 2000 points.""" @@ -24,11 +27,11 @@ def test_validate_phys_data__data_size( assert loop.length() == LOOP_SIZE -def test_validate_phys_data__point_id_type( +def test_validate_physical_data__point_id_type( cif_block: gemmi.cif.Block, ) -> None: """Verify _pd_data.point_id contains integers from 0 to 1999.""" - point_ids = _get_column_values(cif_block, '_pd_data.point_id').astype(int) + point_ids = get_column_values(cif_block, '_pd_data.point_id').astype(int) assert len(point_ids) == LOOP_SIZE assert point_ids[0] == 0 @@ -37,54 +40,50 @@ def test_validate_phys_data__point_id_type( np.testing.assert_array_equal(point_ids, np.arange(LOOP_SIZE)) -def test_validate_phys_data__tof_positive( +def test_validate_physical_data__tof_positive( cif_block: gemmi.cif.Block, ) -> None: """Verify _pd_meas.time_of_flight values are positive floats.""" - tof_values = _get_column_values(cif_block, '_pd_meas.time_of_flight') + tof_values = get_column_values(cif_block, '_pd_meas.time_of_flight') assert np.all(tof_values > 0), 'TOF values must be positive' -def test_validate_phys_data__tof_increasing( +def test_validate_physical_data__tof_increasing( cif_block: gemmi.cif.Block, ) -> None: """Verify _pd_meas.time_of_flight values constantly increase.""" - tof_values = _get_column_values(cif_block, '_pd_meas.time_of_flight') + tof_values = get_column_values(cif_block, '_pd_meas.time_of_flight') assert np.all(np.diff(tof_values) > 0), 'TOF values must be strictly increasing' -def test_validate_phys_data__tof_range( +def test_validate_physical_data__tof_range( cif_block: gemmi.cif.Block, ) -> None: """Verify TOF range: first ~8530.1, last ~66503.7.""" - tof_values = _get_column_values(cif_block, '_pd_meas.time_of_flight') + tof_values = get_column_values(cif_block, '_pd_meas.time_of_flight') assert pytest.approx(tof_values[0], rel=0.01) == 8530.1 assert pytest.approx(tof_values[-1], rel=0.01) == 66503.7 -def test_validate_phys_data__intensity_range( +def test_validate_physical_data__some_intensities( cif_block: gemmi.cif.Block, ) -> None: - """Verify _pd_proc.intensity_norm is non-negative with expected - bounds. - """ - intensity = _get_column_values(cif_block, '_pd_proc.intensity_norm') + """Verify _pd_proc.intensity_norm is non-negative.""" + intensity = get_column_values(cif_block, '_pd_proc.intensity_norm') assert np.all(intensity >= 0), 'Intensity values must be non-negative' assert intensity[0] == pytest.approx(0.0, abs=0.01) assert intensity[-1] == pytest.approx(0.68, rel=0.1) -def test_validate_phys_data__intensity_su( +def test_validate_physical_data__some_intensities_su( cif_block: gemmi.cif.Block, ) -> None: - """Verify _pd_proc.intensity_norm_su is non-negative with expected - bounds. - """ - intensity_su = _get_column_values(cif_block, '_pd_proc.intensity_norm_su') + """Verify _pd_proc.intensity_norm_su is non-negative.""" + intensity_su = get_column_values(cif_block, '_pd_proc.intensity_norm_su') assert np.all(intensity_su >= 0), 'Intensity SU values must be non-negative' assert intensity_su[0] == pytest.approx(0.0, abs=0.01) From d9df094ee996ca90e0fa147f7b79d55cb7727738 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 10 Feb 2026 14:50:20 +0100 Subject: [PATCH 04/15] Enable DREAM reduced-data fitting in tests --- .../dream/test_analyze_reduced_data.py | 67 ++++++++++++------- .../dream/test_validate_meta_data.py | 4 +- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py index e099652b..5c188930 100644 --- a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -36,13 +36,16 @@ def sample_model() -> Any: fract_y=0.125, fract_z=0.125, wyckoff_letter='c', - b_iso=1.96, + b_iso=1.1, ) return model @pytest.fixture(scope='module') -def prepared_cif_path(cif_path: str, tmp_path_factory: pytest.TempPathFactory) -> str: +def prepared_cif_path( + cif_path: str, + tmp_path_factory: pytest.TempPathFactory, +) -> str: """Prepare CIF file with experiment type tags for easydiffraction. """ @@ -66,7 +69,10 @@ def prepared_cif_path(cif_path: str, tmp_path_factory: pytest.TempPathFactory) - @pytest.fixture(scope='module') -def project_with_data(sample_model: Any, prepared_cif_path: str) -> Project: +def project_with_data( + sample_model: Any, + prepared_cif_path: str, +) -> Project: """Create project with sample model and loaded experiment data.""" project = ed.Project() project.sample_models.add(sample_model=sample_model) @@ -83,17 +89,17 @@ def configured_project(project_with_data: Project) -> Project: experiment = project.experiments['reduced_tof'] # Link phase - experiment.linked_phases.add(id='si', scale=1.0) + experiment.linked_phases.add(id='si', scale=0.8) # Instrument setup experiment.instrument.setup_twotheta_bank = 90.0 - experiment.instrument.calib_d_to_tof_linear = 18613.498 + experiment.instrument.calib_d_to_tof_linear = 18630.0 # Peak profile parameters - experiment.peak.broad_gauss_sigma_0 = 48649.0 - experiment.peak.broad_gauss_sigma_1 = 2900.0 + experiment.peak.broad_gauss_sigma_0 = 48500.0 + experiment.peak.broad_gauss_sigma_1 = 3000.0 experiment.peak.broad_gauss_sigma_2 = 0.0 - experiment.peak.broad_mix_beta_0 = 0.0147 + experiment.peak.broad_mix_beta_0 = 0.05 experiment.peak.broad_mix_beta_1 = 0.0 experiment.peak.asym_alpha_0 = 0.0 experiment.peak.asym_alpha_1 = 0.26 @@ -104,14 +110,14 @@ def configured_project(project_with_data: Project) -> Project: # Background points background_points = [ - ('2', 10000, 0.0), - ('3', 14000, 0.26), - ('4', 21000, 0.5), - ('5', 27500, 0.55), - ('6', 40000, 0.31), + ('2', 10000, 0.01), + ('3', 14000, 0.2), + ('4', 21000, 0.7), + ('5', 27500, 0.6), + ('6', 40000, 0.3), ('7', 50000, 0.6), - ('8', 61000, 0.53), - ('9', 70000, 0.58), + ('8', 61000, 0.7), + ('9', 70000, 0.6), ] for id_, x, y in background_points: experiment.background.add(id=id_, x=x, y=y) @@ -120,7 +126,9 @@ def configured_project(project_with_data: Project) -> Project: @pytest.fixture(scope='module') -def fit_results(configured_project: Project) -> dict[str, Any]: +def fit_results( + configured_project: Project, +) -> dict[str, Any]: """Perform fit and return results.""" project = configured_project model = project.sample_models['si'] @@ -137,7 +145,6 @@ def fit_results(configured_project: Project) -> dict[str, Any]: experiment.peak.broad_gauss_sigma_0.free = True experiment.peak.broad_gauss_sigma_1.free = True experiment.peak.broad_mix_beta_0.free = True - experiment.peak.asym_alpha_1.free = True # Run fit project.analysis.fit() @@ -154,12 +161,16 @@ def fit_results(configured_project: Project) -> dict[str, Any]: # ====================================================================== -def test_analyze_data__load_cif(project_with_data: Project) -> None: +def test_analyze_reduced_data__load_cif( + project_with_data: Project, +) -> None: """Verify CIF data loads into project correctly.""" assert 'reduced_tof' in project_with_data.experiments.names -def test_analyze_data__data_size(project_with_data: Project) -> None: +def test_analyze_reduced_data__data_size( + project_with_data: Project, +) -> None: """Verify loaded data has expected size.""" experiment = project_with_data.experiments['reduced_tof'] # Data should have substantial number of points @@ -171,7 +182,9 @@ def test_analyze_data__data_size(project_with_data: Project) -> None: # ====================================================================== -def test_analyze_data__phase_linked(configured_project: Project) -> None: +def test_analyze_data__phase_linked( + configured_project: Project, +) -> None: """Verify phase is correctly linked to experiment.""" experiment = configured_project.experiments['reduced_tof'] # Extract actual id values from linked_phases @@ -181,7 +194,9 @@ def test_analyze_data__phase_linked(configured_project: Project) -> None: assert 'si' in phase_ids -def test_analyze_data__background_set(configured_project: Project) -> None: +def test_analyze_data__background_set( + configured_project: Project, +) -> None: """Verify background points are configured.""" experiment = configured_project.experiments['reduced_tof'] assert len(list(experiment.background)) >= 5 @@ -192,14 +207,16 @@ def test_analyze_data__background_set(configured_project: Project) -> None: # ====================================================================== -@pytest.mark.skip(reason='Fitting not yet working with reduced DREAM data') -def test_analyze_data__fit_success(fit_results: dict[str, Any]) -> None: +def test_analyze_data__fit_success( + fit_results: dict[str, Any], +) -> None: """Verify fitting completes successfully.""" assert fit_results['success'] is True -@pytest.mark.skip(reason='Fitting not yet working with reduced DREAM data') -def test_analyze_data__fit_quality(fit_results: dict[str, Any]) -> None: +def test_analyze_data__fit_quality( + fit_results: dict[str, Any], +) -> None: """Verify fit quality is reasonable (chi-square < threshold).""" # Reduced chi-square should be reasonable for a good fit assert fit_results['reduced_chi'] < 10.0 diff --git a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py index 403faa33..1f129821 100644 --- a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py +++ b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py @@ -54,8 +54,8 @@ def test_validate_meta_data__d_to_tof_difc( loop = table.loop assert loop is not None - id, power, coeff = loop.values - assert id == 'DIFC' + id_, power, coeff = loop.values + assert id_ == 'DIFC' assert int(power) == 1 assert pytest.approx(float(coeff), rel=0.01) == 28385.3 From 4ddedb525734bcb0d130a804a5ef9121ef845e48 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 10 Feb 2026 14:52:02 +0100 Subject: [PATCH 05/15] Update DREAM integration tests for API changes --- .../dream/test_analyze_reduced_data.py | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py index 5c188930..3eabfe21 100644 --- a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -8,6 +8,7 @@ from typing import Any import pytest +from numpy.testing import assert_almost_equal import easydiffraction as ed from easydiffraction import Project @@ -126,10 +127,10 @@ def configured_project(project_with_data: Project) -> Project: @pytest.fixture(scope='module') -def fit_results( +def fitted_project( configured_project: Project, -) -> dict[str, Any]: - """Perform fit and return results.""" +) -> Project: + """Perform fit and return project with results.""" project = configured_project model = project.sample_models['si'] experiment = project.experiments['reduced_tof'] @@ -149,11 +150,7 @@ def fit_results( # Run fit project.analysis.fit() - return { - 'success': project.analysis.fit_results.success, - 'reduced_chi': project.analysis.fit_results.reduced_chi_square, - 'n_free_params': len(project.analysis.fittable_params), - } + return project # ====================================================================== @@ -187,11 +184,7 @@ def test_analyze_data__phase_linked( ) -> None: """Verify phase is correctly linked to experiment.""" experiment = configured_project.experiments['reduced_tof'] - # Extract actual id values from linked_phases - phase_ids = [ - p.id.value if hasattr(p.id, 'value') else str(p.id) for p in experiment.linked_phases - ] - assert 'si' in phase_ids + assert 'si' in experiment.linked_phases.names def test_analyze_data__background_set( @@ -199,7 +192,7 @@ def test_analyze_data__background_set( ) -> None: """Verify background points are configured.""" experiment = configured_project.experiments['reduced_tof'] - assert len(list(experiment.background)) >= 5 + assert len(experiment.background.names) >= 5 # ====================================================================== @@ -207,16 +200,12 @@ def test_analyze_data__background_set( # ====================================================================== -def test_analyze_data__fit_success( - fit_results: dict[str, Any], -) -> None: - """Verify fitting completes successfully.""" - assert fit_results['success'] is True - - def test_analyze_data__fit_quality( - fit_results: dict[str, Any], + fitted_project: Project, ) -> None: """Verify fit quality is reasonable (chi-square < threshold).""" - # Reduced chi-square should be reasonable for a good fit - assert fit_results['reduced_chi'] < 10.0 + assert_almost_equal( + fitted_project.analysis.fit_results.reduced_chi_square, + desired=16.0, + decimal=1, + ) From 16534f7a414a4b0446ba778b74bcd640ed45b9b3 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 10 Feb 2026 15:01:42 +0100 Subject: [PATCH 06/15] Refactor DREAM tests; drop legacy scipp-cif --- pyproject.toml | 8 +- .../scipp-analysis/dream/conftest.py | 28 +++---- .../dream/test_analyze_reduced_data.py | 21 +++--- .../dream/test_package_import.py | 6 +- .../dream/test_read_reduced_data.py | 14 ++-- .../scipp-analysis/dream/test_scipp-cif.py | 74 ------------------- .../dream/test_validate_meta_data.py | 6 +- .../dream/test_validate_physical_data.py | 30 ++++---- 8 files changed, 63 insertions(+), 124 deletions(-) delete mode 100644 tests/integration/scipp-analysis/dream/test_scipp-cif.py diff --git a/pyproject.toml b/pyproject.toml index f152413c..5a77ddf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -190,7 +190,13 @@ fail_under = 65 # Temporarily reduce to allow gradual improvement [tool.ruff] # Temporarily exclude some directories until we have improved the code quality there -exclude = ['tests', 'tmp'] +#exclude = ['tests', 'tmp'] +exclude = [ + 'tmp', + 'tests/unit', + 'tests/integration/fitting', + 'tests/integration/scipp-analysis/tmp', +] indent-width = 4 line-length = 99 # Enable new rules that are not yet stable, like DOC diff --git a/tests/integration/scipp-analysis/dream/conftest.py b/tests/integration/scipp-analysis/dream/conftest.py index c53b308f..d2adc7cf 100644 --- a/tests/integration/scipp-analysis/dream/conftest.py +++ b/tests/integration/scipp-analysis/dream/conftest.py @@ -1,6 +1,10 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2026 DMSC -"""Shared fixtures for DREAM scipp-analysis integration tests.""" +"""Shared fixtures for DREAM scipp-analysis integration tests. + +This module provides pytest fixtures for downloading and parsing +reduced diffraction data from the DREAM instrument in CIF format. +""" from pathlib import Path @@ -8,8 +12,10 @@ import pytest from pooch import retrieve -# CIF file URL and expected data block name +# Remote CIF file URL (regenerated nightly by scipp reduction pipeline) CIF_URL = 'https://pub-6c25ef91903d4301a3338bd53b370098.r2.dev/dream_reduced.cif' + +# Expected datablock name in the CIF file DATABLOCK_NAME = 'reduced_tof' @@ -17,7 +23,11 @@ def cif_path( tmp_path_factory: pytest.TempPathFactory, ) -> str: - """Download CIF file fresh (no caching) and return its path.""" + """Download CIF file fresh each test session and return its path. + + Uses tmp_path_factory to avoid pooch caching, ensuring the latest + version of the nightly-regenerated CIF file is always used. + """ tmp_dir = tmp_path_factory.mktemp('dream_data') return retrieve(url=CIF_URL, known_hash=None, path=tmp_dir) @@ -42,15 +52,5 @@ def cif_document( def cif_block( cif_document: gemmi.cif.Document, ) -> gemmi.cif.Block: - """Return the expected data block from the CIF document.""" + """Return the 'reduced_tof' data block from the CIF document.""" return cif_document.find_block(DATABLOCK_NAME) - - -@pytest.fixture(scope='module') -def data_loop( - cif_block: gemmi.cif.Block, -) -> gemmi.cif.Loop: - """Return the main data loop containing point_id, tof, and intensity - data. - """ - return cif_block.find(['_pd_data.point_id']).loop diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py index 3eabfe21..a47e9384 100644 --- a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -1,7 +1,9 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2026 DMSC -"""Tests for analyzing reduced diffraction data using -easydiffraction. +"""Tests for analyzing reduced diffraction data using easydiffraction. + +These tests verify the complete workflow: loading CIF data, configuring +the experiment, and performing structure refinement. """ from pathlib import Path @@ -14,7 +16,8 @@ from easydiffraction import Project from easydiffraction import SampleModelFactory -# Experiment type tags required for easydiffraction +# CIF experiment type tags required by easydiffraction to identify +# the experiment configuration (powder TOF neutron diffraction) EXPT_TYPE_TAGS = { '_expt_type.sample_form': 'powder', '_expt_type.beam_mode': 'time-of-flight', @@ -153,9 +156,7 @@ def fitted_project( return project -# ====================================================================== # Test: Data Loading -# ====================================================================== def test_analyze_reduced_data__load_cif( @@ -174,12 +175,10 @@ def test_analyze_reduced_data__data_size( assert experiment.data.x.size > 100 -# ====================================================================== # Test: Configuration -# ====================================================================== -def test_analyze_data__phase_linked( +def test_analyze_reduced_data__phase_linked( configured_project: Project, ) -> None: """Verify phase is correctly linked to experiment.""" @@ -187,7 +186,7 @@ def test_analyze_data__phase_linked( assert 'si' in experiment.linked_phases.names -def test_analyze_data__background_set( +def test_analyze_reduced_data__background_set( configured_project: Project, ) -> None: """Verify background points are configured.""" @@ -195,12 +194,10 @@ def test_analyze_data__background_set( assert len(experiment.background.names) >= 5 -# ====================================================================== # Test: Fitting -# ====================================================================== -def test_analyze_data__fit_quality( +def test_analyze_reduced_data__fit_quality( fitted_project: Project, ) -> None: """Verify fit quality is reasonable (chi-square < threshold).""" diff --git a/tests/integration/scipp-analysis/dream/test_package_import.py b/tests/integration/scipp-analysis/dream/test_package_import.py index b5223910..e0607898 100644 --- a/tests/integration/scipp-analysis/dream/test_package_import.py +++ b/tests/integration/scipp-analysis/dream/test_package_import.py @@ -1,6 +1,10 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2026 DMSC -"""Tests for verifying package installation and importability.""" +"""Tests for verifying package installation and version consistency. + +These tests check that easydiffraction and essdiffraction packages are +installed and match the latest PyPI release (MAJOR.MINOR.PATCH only). +""" import importlib.metadata diff --git a/tests/integration/scipp-analysis/dream/test_read_reduced_data.py b/tests/integration/scipp-analysis/dream/test_read_reduced_data.py index 51f26d99..616c9876 100644 --- a/tests/integration/scipp-analysis/dream/test_read_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_read_reduced_data.py @@ -1,6 +1,10 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2026 DMSC -"""Tests for reading reduced data from CIF files.""" +"""Tests for reading reduced data from CIF files. + +These tests verify that the CIF file can be fetched, read as text, +and parsed by gemmi without errors. +""" import gemmi @@ -16,11 +20,9 @@ def test_read_reduced_data__fetch_cif( def test_read_reduced_data__py_read_cif( cif_content: str, ) -> None: - """Verify that the CIF file can be read as text and has correct - format. - """ - assert len(cif_content) > 0 # Check file is not empty - assert '#\\#CIF_1.1' in cif_content # Check CIF version is 1.1 + """Verify CIF file can be read as text with CIF 1.1 header.""" + assert len(cif_content) > 0 + assert '#\\#CIF_1.1' in cif_content def test_read_reduced_data__gemmi_parse_cif( diff --git a/tests/integration/scipp-analysis/dream/test_scipp-cif.py b/tests/integration/scipp-analysis/dream/test_scipp-cif.py deleted file mode 100644 index 15a49b6f..00000000 --- a/tests/integration/scipp-analysis/dream/test_scipp-cif.py +++ /dev/null @@ -1,74 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -import numpy as np -from numpy.testing import assert_array_equal -from pooch import retrieve - -import easydiffraction as ed - - -def test_read_tof_cif_from_scipp() -> None: - """ - Test reading a CIF file from scipp - :return: None - """ - - # Retrieve the CIF file - file_path = retrieve( - url="https://pub-6c25ef91903d4301a3338bd53b370098.r2.dev/dream_reduced.cif", - known_hash=None, - ) - - # Add experiment type - expt_type = { - '_expt_type.sample_form': 'powder', - '_expt_type.beam_mode': 'time-of-flight', - '_expt_type.radiation_probe': 'neutron', - '_expt_type.scattering_type': 'bragg', - } - with open(file_path) as f: - content = f.read() - for key, value in expt_type.items(): - if key not in content: - with open(file_path, "a") as f: - f.write(f"{key} {value}\n") - - # Create project - proj = ed.Project() - - # Add experiment from CIF file - proj.experiments.add(cif_path=file_path) - - # Check the experiment names - assert proj.experiments.names == ['reduced_tof'] - - # Alias for easier access - experiment = proj.experiments['reduced_tof'] - - # Check data size - assert experiment.data.x.size == 200 - - # Check some x data points - assert_array_equal( - experiment.data.x[:4], - np.array([ - 57.526660478722604, - 172.57998143616783, - 287.633302393613, - 402.68662335105824, - ]) - ) - assert_array_equal( - experiment.data.x[-2:], - np.array([ - 22838.084210052875, - 22953.137531010318, - ]) - ) - - # Check some measured y data points - #assert experiment.data.meas[93] == 2.0 - - # Check some uncertainty data points - #assert experiment.data.meas_su[93] == 1.4142135623730951 diff --git a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py index 1f129821..7712dbc3 100644 --- a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py +++ b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py @@ -1,6 +1,10 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2026 DMSC -"""Tests for validating metadata structure in CIF files.""" +"""Tests for validating metadata structure in CIF files. + +These tests verify that the CIF file contains the expected data blocks, +loops, and calibration parameters required for TOF diffraction analysis. +""" import gemmi import pytest diff --git a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py index 2bb51f1b..49eea29e 100644 --- a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py +++ b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py @@ -1,12 +1,16 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2026 DMSC -"""Tests for validating physical data values in CIF files.""" +"""Tests for validating physical data values in CIF files. + +These tests verify that numerical data columns contain valid, +physically meaningful values (positive TOF, non-negative intensity). +""" import gemmi import numpy as np import pytest -# Expected number of data points in the loop +# Expected number of data points in the loop (from scipp reduction) LOOP_SIZE = 2000 @@ -14,7 +18,7 @@ def get_column_values( cif_block: gemmi.cif.Block, tag: str, ) -> np.ndarray: - """Helper to extract column values as numpy array.""" + """Extract column values from CIF block as numpy array.""" column = cif_block.find([tag]) return np.array([float(row[0]) for row in column]) @@ -30,48 +34,44 @@ def test_validate_physical_data__data_size( def test_validate_physical_data__point_id_type( cif_block: gemmi.cif.Block, ) -> None: - """Verify _pd_data.point_id contains integers from 0 to 1999.""" + """Verify _pd_data.point_id contains sequential integers.""" point_ids = get_column_values(cif_block, '_pd_data.point_id').astype(int) assert len(point_ids) == LOOP_SIZE assert point_ids[0] == 0 assert point_ids[-1] == LOOP_SIZE - 1 - # Verify sequential integers np.testing.assert_array_equal(point_ids, np.arange(LOOP_SIZE)) def test_validate_physical_data__tof_positive( cif_block: gemmi.cif.Block, ) -> None: - """Verify _pd_meas.time_of_flight values are positive floats.""" + """Verify _pd_meas.time_of_flight values are positive.""" tof_values = get_column_values(cif_block, '_pd_meas.time_of_flight') - assert np.all(tof_values > 0), 'TOF values must be positive' def test_validate_physical_data__tof_increasing( cif_block: gemmi.cif.Block, ) -> None: - """Verify _pd_meas.time_of_flight values constantly increase.""" + """Verify _pd_meas.time_of_flight values are strictly increasing.""" tof_values = get_column_values(cif_block, '_pd_meas.time_of_flight') - assert np.all(np.diff(tof_values) > 0), 'TOF values must be strictly increasing' def test_validate_physical_data__tof_range( cif_block: gemmi.cif.Block, ) -> None: - """Verify TOF range: first ~8530.1, last ~66503.7.""" + """Verify TOF range spans approx. 8530 to 66504 microseconds.""" tof_values = get_column_values(cif_block, '_pd_meas.time_of_flight') - assert pytest.approx(tof_values[0], rel=0.01) == 8530.1 assert pytest.approx(tof_values[-1], rel=0.01) == 66503.7 -def test_validate_physical_data__some_intensities( +def test_validate_physical_data__intensity_values( cif_block: gemmi.cif.Block, ) -> None: - """Verify _pd_proc.intensity_norm is non-negative.""" + """Verify _pd_proc.intensity_norm values are non-negative.""" intensity = get_column_values(cif_block, '_pd_proc.intensity_norm') assert np.all(intensity >= 0), 'Intensity values must be non-negative' @@ -79,10 +79,10 @@ def test_validate_physical_data__some_intensities( assert intensity[-1] == pytest.approx(0.68, rel=0.1) -def test_validate_physical_data__some_intensities_su( +def test_validate_physical_data__intensity_su_values( cif_block: gemmi.cif.Block, ) -> None: - """Verify _pd_proc.intensity_norm_su is non-negative.""" + """Verify _pd_proc.intensity_norm_su values are non-negative.""" intensity_su = get_column_values(cif_block, '_pd_proc.intensity_norm_su') assert np.all(intensity_su >= 0), 'Intensity SU values must be non-negative' From fab6dda8079515875ca32b16c5bfa5bc016b85aa Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 10 Feb 2026 15:02:08 +0100 Subject: [PATCH 07/15] Round 2theta range to 5dp in CIF output --- src/easydiffraction/analysis/calculators/cryspy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/easydiffraction/analysis/calculators/cryspy.py b/src/easydiffraction/analysis/calculators/cryspy.py index a24fda7f..18e7858d 100644 --- a/src/easydiffraction/analysis/calculators/cryspy.py +++ b/src/easydiffraction/analysis/calculators/cryspy.py @@ -361,8 +361,8 @@ def _convert_experiment_to_cryspy_cif( cif_lines.append(f'{engine_key_name} {attr_obj.value}') x_data = experiment.data.x - twotheta_min = float(x_data.min()) - twotheta_max = float(x_data.max()) + twotheta_min = f'{np.round(x_data.min(), 5):.5f}' # float(x_data.min()) + twotheta_max = f'{np.round(x_data.max(), 5):.5f}' # float(x_data.max()) cif_lines.append('') if expt_type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: cif_lines.append(f'_range_2theta_min {twotheta_min}') From feb970a2de9be6062624e9dafbce0e24319aa50b Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 10 Feb 2026 22:06:55 +0100 Subject: [PATCH 08/15] Update data index SHA256 hash --- src/easydiffraction/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/easydiffraction/utils/utils.py b/src/easydiffraction/utils/utils.py index 13b90b16..2b6f9c50 100644 --- a/src/easydiffraction/utils/utils.py +++ b/src/easydiffraction/utils/utils.py @@ -74,7 +74,7 @@ def _fetch_data_index() -> dict: _validate_url(index_url) # macOS: sha256sum index.json - index_hash = 'sha256:e78f5dd2f229ea83bfeb606502da602fc0b07136889877d3ab601694625dd3d7' + index_hash = 'sha256:9aceaf51d298992058c80903283c9a83543329a063692d49b7aaee1156e76884' destination_dirname = 'easydiffraction' destination_fname = 'data-index.json' cache_dir = pooch.os_cache(destination_dirname) From 257000e4ca78e24d8d9fbd3e32729ca06871b44c Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 10 Feb 2026 22:23:05 +0100 Subject: [PATCH 09/15] Relax version check to support dev builds --- .../scipp-analysis/dream/test_package_import.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/integration/scipp-analysis/dream/test_package_import.py b/tests/integration/scipp-analysis/dream/test_package_import.py index e0607898..7c10d02b 100644 --- a/tests/integration/scipp-analysis/dream/test_package_import.py +++ b/tests/integration/scipp-analysis/dream/test_package_import.py @@ -3,7 +3,7 @@ """Tests for verifying package installation and version consistency. These tests check that easydiffraction and essdiffraction packages are -installed and match the latest PyPI release (MAJOR.MINOR.PATCH only). +installed and are not older than the latest PyPI release. """ import importlib.metadata @@ -47,11 +47,14 @@ def get_base_version( @pytest.mark.parametrize('package_name', PACKAGE_NAMES) -def test_package_import__latest( +def test_package_import( package_name: str, ) -> None: - """Verify installed package matches PyPI latest version - (MAJOR.MINOR.PATCH). + """Verify installed package is not older than PyPI latest version. + + Uses >= comparison to support both: + - Real releases where installed == latest + - Dev builds where installed (e.g., 999.0.0) > latest """ installed_version = get_installed_version(package_name) latest_version = get_latest_version(package_name) @@ -60,9 +63,9 @@ def test_package_import__latest( assert latest_version is not None, f'Could not fetch latest version for {package_name}.' # Compare only MAJOR.MINOR.PATCH, ignoring local version identifiers - installed_base = get_base_version(installed_version) - latest_base = get_base_version(latest_version) + installed_base = Version(get_base_version(installed_version)) + latest_base = Version(get_base_version(latest_version)) - assert installed_base == latest_base, ( + assert installed_base >= latest_base, ( f'Package {package_name} is outdated: Installed={installed_base}, Latest={latest_base}' ) From 3bc98f25f4f8a55120bac27943d3d8e14accb2f7 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 10 Feb 2026 22:34:18 +0100 Subject: [PATCH 10/15] Add essdiffraction dependency --- .github/workflows/coverage.yaml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index cd212177..96ae3386 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -86,7 +86,7 @@ jobs: verbose: true token: ${{ secrets.CODECOV_TOKEN }} - # Job 2: Run integration tests with coverage and upload to Codecov + # Job 3: Run integration tests with coverage and upload to Codecov integration-tests-coverage: runs-on: ubuntu-latest diff --git a/pyproject.toml b/pyproject.toml index 5a77ddf7..6076069b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ ] requires-python = '>=3.11,<3.14' dependencies = [ + 'essdiffraction', # ESS Diffraction library 'numpy', # Numerical computing library 'colorama', # Color terminal output 'tabulate', # Pretty-print tabular data for terminal output From e86087457424884355154e69f9c89daa50480f4a Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 11 Feb 2026 08:51:40 +0100 Subject: [PATCH 11/15] Prevent zero SUs; default intensity SU to 1.0 --- .../experiments/categories/data/bragg_pd.py | 19 +++++++++++++++++-- .../dream/test_analyze_reduced_data.py | 3 --- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/easydiffraction/experiments/categories/data/bragg_pd.py b/src/easydiffraction/experiments/categories/data/bragg_pd.py index 47bdf1cb..da92da30 100644 --- a/src/easydiffraction/experiments/categories/data/bragg_pd.py +++ b/src/easydiffraction/experiments/categories/data/bragg_pd.py @@ -76,7 +76,7 @@ def __init__(self, **kwargs): description='Standard uncertainty of the measured intensity at this data point.', value_spec=AttributeSpec( type_=DataTypes.NUMERIC, - default=0.0, + default=1.0, content_validator=RangeValidator(ge=0), ), cif_handler=CifHandler( @@ -321,7 +321,22 @@ def meas(self) -> np.ndarray: @property def meas_su(self) -> np.ndarray: - return np.fromiter((p.intensity_meas_su.value for p in self._calc_items), dtype=float) + # TODO: The following is a temporary workaround to handle zero + # or near-zero uncertainties in the data, when dats is loaded + # from CIF files. This is necessary because zero uncertainties + # cause fitting algorithms to fail. + # The current implementation is inefficient. + # In the future, we should extend the functionality of + # the NumericDescriptor to automatically replace the value + # outside of the valid range (`content_validator`) with a + # default value (`default`), when the value is set. + # BraggPdExperiment._load_ascii_data_to_experiment() handles + # this for ASCII data, but we also need to handle CIF data and + # come up with a consistent approach for both data sources. + original = np.fromiter((p.intensity_meas_su.value for p in self._calc_items), dtype=float) + # Replace values smaller than 0.0001 with 1.0 + modified = np.where(original < 0.0001, 1.0, original) + return modified @property def calc(self) -> np.ndarray: diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py index a47e9384..3a1424fa 100644 --- a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -56,9 +56,6 @@ def prepared_cif_path( with Path(cif_path).open() as f: content = f.read() - # Replace zero ESDs with finite values (required for fitting) - content = content.replace(' 0.0 0.0', ' 0.0 1.0') - # Add experiment type tags if missing for tag, value in EXPT_TYPE_TAGS.items(): if tag not in content: From 4e1f3fa5c174c235c08bb8f8d71080ac4f35c220 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 11 Feb 2026 10:16:14 +0100 Subject: [PATCH 12/15] Update tests/integration/scipp-analysis/dream/conftest.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/integration/scipp-analysis/dream/conftest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration/scipp-analysis/dream/conftest.py b/tests/integration/scipp-analysis/dream/conftest.py index d2adc7cf..e244add6 100644 --- a/tests/integration/scipp-analysis/dream/conftest.py +++ b/tests/integration/scipp-analysis/dream/conftest.py @@ -53,4 +53,8 @@ def cif_block( cif_document: gemmi.cif.Document, ) -> gemmi.cif.Block: """Return the 'reduced_tof' data block from the CIF document.""" - return cif_document.find_block(DATABLOCK_NAME) + block = cif_document.find_block(DATABLOCK_NAME) + assert block is not None, ( + f"Expected CIF datablock {DATABLOCK_NAME!r} was not found in the document." + ) + return block From 70179c5b91f9811d5e1948539330c7697c386c18 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 11 Feb 2026 10:36:06 +0100 Subject: [PATCH 13/15] Relax point ID check; allow conftest asserts --- pyproject.toml | 3 ++- tests/integration/scipp-analysis/dream/conftest.py | 2 +- .../scipp-analysis/dream/test_validate_physical_data.py | 8 ++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6076069b..fb0118e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -271,7 +271,8 @@ ban-relative-imports = 'all' force-single-line = true [tool.ruff.lint.per-file-ignores] -'*test_*.py' = ['S101'] # allow asserts in test files +'*test_*.py' = ['S101'] # allow asserts in test files +'conftest.py' = ['S101'] # allow asserts in test files # Vendored jupyter_dark_detect: keep as-is from upstream for easy updates # https://github.com/OpenMined/jupyter-dark-detect/tree/main/jupyter_dark_detect 'src/easydiffraction/utils/_vendored/jupyter_dark_detect/*' = [ diff --git a/tests/integration/scipp-analysis/dream/conftest.py b/tests/integration/scipp-analysis/dream/conftest.py index e244add6..56aabf1f 100644 --- a/tests/integration/scipp-analysis/dream/conftest.py +++ b/tests/integration/scipp-analysis/dream/conftest.py @@ -55,6 +55,6 @@ def cif_block( """Return the 'reduced_tof' data block from the CIF document.""" block = cif_document.find_block(DATABLOCK_NAME) assert block is not None, ( - f"Expected CIF datablock {DATABLOCK_NAME!r} was not found in the document." + f'Expected CIF datablock {DATABLOCK_NAME!r} was not found in the document.' ) return block diff --git a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py index 49eea29e..f1be5eb3 100644 --- a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py +++ b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py @@ -35,12 +35,16 @@ def test_validate_physical_data__point_id_type( cif_block: gemmi.cif.Block, ) -> None: """Verify _pd_data.point_id contains sequential integers.""" - point_ids = get_column_values(cif_block, '_pd_data.point_id').astype(int) + point_ids = get_column_values(cif_block, '_pd_data.point_id') + + # Ensure all point IDs are integer-valued (no fractional part). + frac, _ = np.modf(point_ids) + assert np.all(frac == 0), '_pd_data.point_id values are expected to be integers' assert len(point_ids) == LOOP_SIZE assert point_ids[0] == 0 assert point_ids[-1] == LOOP_SIZE - 1 - np.testing.assert_array_equal(point_ids, np.arange(LOOP_SIZE)) + np.testing.assert_array_equal(point_ids, np.arange(LOOP_SIZE, dtype=point_ids.dtype)) def test_validate_physical_data__tof_positive( From 930f36c783d7702050f540daa7912e4afba68be8 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 11 Feb 2026 10:55:44 +0100 Subject: [PATCH 14/15] Inline sample model and consolidate test fixtures --- .../dream/test_analyze_reduced_data.py | 122 ++++++++++-------- 1 file changed, 67 insertions(+), 55 deletions(-) diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py index 3a1424fa..d804e79d 100644 --- a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -2,19 +2,22 @@ # Copyright (c) 2026 DMSC """Tests for analyzing reduced diffraction data using easydiffraction. -These tests verify the complete workflow: loading CIF data, configuring -the experiment, and performing structure refinement. +These tests verify the complete workflow: +1. Define project +2. Add sample model manually defined +3. Modify experiment CIF file +4. Add experiment from modified CIF file +5. Modify default experiment configuration +6. Select parameters to be fitted +7. Do fitting """ from pathlib import Path -from typing import Any import pytest from numpy.testing import assert_almost_equal import easydiffraction as ed -from easydiffraction import Project -from easydiffraction import SampleModelFactory # CIF experiment type tags required by easydiffraction to identify # the experiment configuration (powder TOF neutron diffraction) @@ -26,25 +29,6 @@ } -@pytest.fixture(scope='module') -def sample_model() -> Any: - """Create a Silicon sample model for fitting.""" - model = SampleModelFactory.create(name='si') - model.space_group.name_h_m = 'F d -3 m' - model.space_group.it_coordinate_system_code = '1' - model.cell.length_a = 5.43146 - model.atom_sites.add( - label='Si', - type_symbol='Si', - fract_x=0.125, - fract_y=0.125, - fract_z=0.125, - wyckoff_letter='c', - b_iso=1.1, - ) - return model - - @pytest.fixture(scope='module') def prepared_cif_path( cif_path: str, @@ -71,24 +55,44 @@ def prepared_cif_path( @pytest.fixture(scope='module') def project_with_data( - sample_model: Any, prepared_cif_path: str, -) -> Project: - """Create project with sample model and loaded experiment data.""" +) -> ed.Project: + """Create project with sample model, experiment data, and + configuration. + + 1. Define project + 2. Add sample model manually defined + 3. Modify experiment CIF file + 4. Add experiment from modified CIF file + 5. Modify default experiment configuration + """ + # Step 1: Define Project project = ed.Project() - project.sample_models.add(sample_model=sample_model) - project.experiments.add(cif_path=prepared_cif_path) - return project + # Step 2: Define Sample Model manually + project.sample_models.add(name='si') + sample_model = project.sample_models['si'] -@pytest.fixture(scope='module') -def configured_project(project_with_data: Project) -> Project: - """Configure project with instrument and peak parameters for - fitting. - """ - project = project_with_data + sample_model.space_group.name_h_m = 'F d -3 m' + sample_model.space_group.it_coordinate_system_code = '1' + + sample_model.cell.length_a = 5.43146 + + sample_model.atom_sites.add( + label='Si', + type_symbol='Si', + fract_x=0.125, + fract_y=0.125, + fract_z=0.125, + wyckoff_letter='c', + b_iso=1.1, + ) + + # Step 3: Add experiment from modified CIF file + project.experiments.add(cif_path=prepared_cif_path) experiment = project.experiments['reduced_tof'] + # Step 4: Configure experiment # Link phase experiment.linked_phases.add(id='si', scale=0.8) @@ -128,26 +132,34 @@ def configured_project(project_with_data: Project) -> Project: @pytest.fixture(scope='module') def fitted_project( - configured_project: Project, -) -> Project: - """Perform fit and return project with results.""" - project = configured_project - model = project.sample_models['si'] + project_with_data: ed.Project, +) -> ed.Project: + """Perform fit and return project with results. + + 6. Select parameters to be fitted + 7. Do fitting + """ + project = project_with_data + sample_model = project.sample_models['si'] experiment = project.experiments['reduced_tof'] - # Set free parameters for background - for point in experiment.background: - point.y.free = True + # Step 5: Select parameters to be fitted + # Set free parameters for sample model + sample_model.atom_sites['Si'].b_iso.free = True - # Set free parameters for fitting - model.atom_sites['Si'].b_iso.free = True + # Set free parameters for experiment experiment.linked_phases['si'].scale.free = True experiment.instrument.calib_d_to_tof_linear.free = True + experiment.peak.broad_gauss_sigma_0.free = True experiment.peak.broad_gauss_sigma_1.free = True experiment.peak.broad_mix_beta_0.free = True - # Run fit + # Set free parameters for background + for point in experiment.background: + point.y.free = True + + # Step 6: Do fitting project.analysis.fit() return project @@ -157,14 +169,14 @@ def fitted_project( def test_analyze_reduced_data__load_cif( - project_with_data: Project, + project_with_data: ed.Project, ) -> None: """Verify CIF data loads into project correctly.""" assert 'reduced_tof' in project_with_data.experiments.names def test_analyze_reduced_data__data_size( - project_with_data: Project, + project_with_data: ed.Project, ) -> None: """Verify loaded data has expected size.""" experiment = project_with_data.experiments['reduced_tof'] @@ -176,18 +188,18 @@ def test_analyze_reduced_data__data_size( def test_analyze_reduced_data__phase_linked( - configured_project: Project, + project_with_data: ed.Project, ) -> None: """Verify phase is correctly linked to experiment.""" - experiment = configured_project.experiments['reduced_tof'] + experiment = project_with_data.experiments['reduced_tof'] assert 'si' in experiment.linked_phases.names def test_analyze_reduced_data__background_set( - configured_project: Project, + project_with_data: ed.Project, ) -> None: """Verify background points are configured.""" - experiment = configured_project.experiments['reduced_tof'] + experiment = project_with_data.experiments['reduced_tof'] assert len(experiment.background.names) >= 5 @@ -195,9 +207,9 @@ def test_analyze_reduced_data__background_set( def test_analyze_reduced_data__fit_quality( - fitted_project: Project, + fitted_project: ed.Project, ) -> None: - """Verify fit quality is reasonable (chi-square < threshold).""" + """Verify fit quality is reasonable (chi-square value).""" assert_almost_equal( fitted_project.analysis.fit_results.reduced_chi_square, desired=16.0, From 44ec50be55fc13c122a06af84eb5de5859ca5919 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 11 Feb 2026 10:58:50 +0100 Subject: [PATCH 15/15] Replace assert_almost_equal with pytest.approx --- .../scipp-analysis/dream/test_analyze_reduced_data.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py index d804e79d..eb528ff5 100644 --- a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -15,7 +15,6 @@ from pathlib import Path import pytest -from numpy.testing import assert_almost_equal import easydiffraction as ed @@ -210,8 +209,5 @@ def test_analyze_reduced_data__fit_quality( fitted_project: ed.Project, ) -> None: """Verify fit quality is reasonable (chi-square value).""" - assert_almost_equal( - fitted_project.analysis.fit_results.reduced_chi_square, - desired=16.0, - decimal=1, - ) + chi_square = fitted_project.analysis.fit_results.reduced_chi_square + assert chi_square == pytest.approx(16.0, abs=0.1)