diff --git a/docs/user-guide/dream/dream-data-reduction.ipynb b/docs/user-guide/dream/dream-data-reduction.ipynb index 9e4aada..00f6bd1 100644 --- a/docs/user-guide/dream/dream-data-reduction.ipynb +++ b/docs/user-guide/dream/dream-data-reduction.ipynb @@ -20,8 +20,7 @@ "outputs": [], "source": [ "import scipp as sc\n", - "import scippneutron as scn\n", - "import scippneutron.io\n", + "from scippneutron.io import cif\n", "\n", "from ess import dream, powder\n", "import ess.dream.data # noqa: F401\n", @@ -87,6 +86,31 @@ "cell_type": "markdown", "id": "6", "metadata": {}, + "source": [ + "We also need some parameters to configure the output file:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "workflow[CIFAuthors] = CIFAuthors([\n", + " cif.Author(\n", + " name=\"Jane Doe\",\n", + " email=\"jane.doe@ess.eu\",\n", + " orcid=\"0000-0000-0000-0001\",\n", + " role=\"measurement\",\n", + " ),\n", + "])" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, "source": [ "## Use the workflow\n", "\n", @@ -96,7 +120,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -105,27 +129,29 @@ }, { "cell_type": "markdown", - "id": "8", + "id": "10", "metadata": {}, "source": [ - "We then call `compute()` to compute the result:" + "We then call `compute()` to compute the result:\n", + "(The `cif` object will later be used to write the result to disk.)" ] }, { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "11", "metadata": {}, "outputs": [], "source": [ - "result = workflow.compute(IofDspacing)\n", - "result" + "results = workflow.compute([IofDspacing, ReducedDspacingCIF])\n", + "result = results[IofDspacing]\n", + "cif_data = results[ReducedDspacingCIF]" ] }, { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "12", "metadata": {}, "outputs": [], "source": [ @@ -135,28 +161,30 @@ }, { "cell_type": "markdown", - "id": "11", + "id": "13", "metadata": {}, "source": [ - "We can now save the result to disk:" + "We can now save the result to disk:\n", + "(The comment is optional but helps to identify the file later.)" ] }, { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "14", "metadata": {}, "outputs": [], "source": [ - "dspacing_histogram.coords[\"dspacing\"] = sc.midpoints(\n", - " dspacing_histogram.coords[\"dspacing\"]\n", - ")\n", - "scn.io.save_xye(\"dspacing.xye\", dspacing_histogram)" + "cif_data.comment = \"\"\"This file was generated with the DREAM data reduction user guide\n", + "in the documentation of ESSdiffraction.\n", + "See https://scipp.github.io/essdiffraction/\n", + "\"\"\"\n", + "cif_data.save('dspacing.cif')" ] }, { "cell_type": "markdown", - "id": "13", + "id": "15", "metadata": {}, "source": [ "## Compute intermediate results\n", @@ -169,7 +197,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -186,7 +214,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -198,7 +226,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "18", "metadata": {}, "source": [ "## Grouping by scattering angle\n", @@ -210,7 +238,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -222,7 +250,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -233,7 +261,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -251,7 +279,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -275,7 +303,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml index cf7825a..fc6d00a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,8 @@ dependencies = [ "plopp", "pythreejs", "sciline>=24.06.0", - "scipp>=24.09.1", # Fixed new hist/bin API - "scippneutron>=24.5.0", + "scipp>=24.09.1", + "scippneutron>=24.9.0", "scippnexus>=23.12.0", ] diff --git a/requirements/base.in b/requirements/base.in index 5f380cc..a0365d0 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -10,5 +10,5 @@ plopp pythreejs sciline>=24.06.0 scipp>=24.09.1 -scippneutron>=24.5.0 +scippneutron>=24.9.0 scippnexus>=23.12.0 diff --git a/requirements/base.txt b/requirements/base.txt index 1e91667..638a29f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:6ce1bbccfc85ced70dedc62a2266b339f1e7c814 +# SHA1:2cad3df672e12033fa21254667278ceafae15e09 # # This file is autogenerated by pip-compile-multi # To update, run: diff --git a/requirements/ci.txt b/requirements/ci.txt index 28ed239..af1677b 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -38,7 +38,7 @@ platformdirs==4.3.6 # virtualenv pluggy==1.5.0 # via tox -pyproject-api==1.7.2 +pyproject-api==1.8.0 # via tox requests==2.32.3 # via -r ci.in @@ -48,7 +48,7 @@ tomli==2.0.1 # via # pyproject-api # tox -tox==4.19.0 +tox==4.20.0 # via -r ci.in urllib3==2.2.3 # via requests diff --git a/requirements/docs.txt b/requirements/docs.txt index 026f58a..3bea4bd 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -160,7 +160,7 @@ sphinx==8.0.2 # sphinx-copybutton # sphinx-design # sphinxcontrib-bibtex -sphinx-autodoc-typehints==2.4.3 +sphinx-autodoc-typehints==2.4.4 # via -r docs.in sphinx-copybutton==0.5.2 # via -r docs.in diff --git a/src/ess/dream/__init__.py b/src/ess/dream/__init__.py index a6a8350..68aae7b 100644 --- a/src/ess/dream/__init__.py +++ b/src/ess/dream/__init__.py @@ -12,18 +12,17 @@ from .workflow import DreamGeant4Workflow, default_parameters try: - __version__ = importlib.metadata.version(__package__ or __name__) + __version__ = importlib.metadata.version("essdiffraction") except importlib.metadata.PackageNotFoundError: __version__ = "0.0.0" del importlib __all__ = [ + '__version__', 'DreamGeant4Workflow', 'default_parameters', - 'beamline', 'instrument_view', 'load_geant4_csv', 'nexus', - 'providers', ] diff --git a/src/ess/dream/io/__init__.py b/src/ess/dream/io/__init__.py index 2224a07..5a53ba3 100644 --- a/src/ess/dream/io/__init__.py +++ b/src/ess/dream/io/__init__.py @@ -5,5 +5,8 @@ from . import nexus from .geant4 import load_geant4_csv +from .cif import prepare_reduced_dspacing_cif -__all__ = ["nexus", "load_geant4_csv"] +providers = (prepare_reduced_dspacing_cif,) + +__all__ = ["nexus", "load_geant4_csv", "prepare_reduced_dspacing_cif", "providers"] diff --git a/src/ess/dream/io/cif.py b/src/ess/dream/io/cif.py new file mode 100644 index 0000000..c99d635 --- /dev/null +++ b/src/ess/dream/io/cif.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) + +"""CIF writer for DREAM.""" + +import scipp as sc +from scippneutron.io import cif + +from ess.powder.types import CIFAuthors, IofDspacing, ReducedDspacingCIF + + +def prepare_reduced_dspacing_cif( + da: IofDspacing, *, authors: CIFAuthors +) -> ReducedDspacingCIF: + """Construct a CIF builder with reduced data in d-spacing. + + The object contains the d-spacing coordinate, intensities, + and some metadata. + + Parameters + ---------- + da: + Reduced 1d data with a `'dspacing'` dimension and coordinate. + authors: + List of authors to write to the file. + + Returns + ------- + : + An object that contains the reduced data and metadata. + Us its ``save`` method to write the CIF file. + """ + from .. import __version__ + + to_save = _prepare_data(da) + return ReducedDspacingCIF( + cif.CIF('reduced_dspacing') + .with_reducers(f'ess.dream v{__version__}') + .with_authors(*authors) + .with_beamline(beamline='DREAM', facility='ESS') + .with_reduced_powder_data(to_save) + ) + + +def _prepare_data(da: sc.DataArray) -> sc.DataArray: + hist = da.copy(deep=False) if da.bins is None else da.hist() + hist.coords[hist.dim] = sc.midpoints(hist.coords[hist.dim]) + return hist diff --git a/src/ess/dream/workflow.py b/src/ess/dream/workflow.py index fc23813..d959c9c 100644 --- a/src/ess/dream/workflow.py +++ b/src/ess/dream/workflow.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2024 Scipp contributors (https://github.com/scipp) +import itertools + import sciline import scipp as sc @@ -13,8 +15,11 @@ VanadiumRun, ) +from .io.cif import CIFAuthors, prepare_reduced_dspacing_cif from .io.geant4 import LoadGeant4Workflow +_dream_providers = (prepare_reduced_dspacing_cif,) + def default_parameters() -> dict: # Quantities not available in the simulated data @@ -28,6 +33,7 @@ def default_parameters() -> dict: NeXusSource[VanadiumRun]: source, AccumulatedProtonCharge[SampleRun]: charge, AccumulatedProtonCharge[VanadiumRun]: charge, + CIFAuthors: CIFAuthors([]), } @@ -36,7 +42,7 @@ def DreamGeant4Workflow() -> sciline.Pipeline: Workflow with default parameters for the Dream Geant4 simulation. """ wf = LoadGeant4Workflow() - for provider in powder_providers: + for provider in itertools.chain(powder_providers, _dream_providers): wf.insert(provider) for key, value in default_parameters().items(): wf[key] = value diff --git a/src/ess/powder/__init__.py b/src/ess/powder/__init__.py index 3ce316e..9ff2327 100644 --- a/src/ess/powder/__init__.py +++ b/src/ess/powder/__init__.py @@ -18,7 +18,7 @@ from .masking import with_pixel_mask_filenames try: - __version__ = importlib.metadata.version(__package__ or __name__) + __version__ = importlib.metadata.version("essdiffraction") except importlib.metadata.PackageNotFoundError: __version__ = "0.0.0" @@ -35,12 +35,12 @@ """Sciline providers for powder diffraction.""" __all__ = [ + "__version__", "conversion", "correction", "filtering", "grouping", "masking", - "nexus", "transform", "providers", "smoothing", diff --git a/src/ess/powder/types.py b/src/ess/powder/types.py index dc8d5d4..c86fd4d 100644 --- a/src/ess/powder/types.py +++ b/src/ess/powder/types.py @@ -12,6 +12,7 @@ import sciline import scipp as sc +from scippneutron.io import cif from ess.reduce.nexus import generic_types as reduce_gt from ess.reduce.nexus import types as reduce_t @@ -165,4 +166,10 @@ class RawDataAndMetadata(sciline.Scope[RunType, sc.DataGroup], sc.DataGroup): """WavelengthMask is a callable that returns a mask for a given WavelengthData.""" +CIFAuthors = NewType('CIFAuthors', list[cif.Author]) +"""List of authors to save to output CIF files.""" + +ReducedDspacingCIF = NewType('ReducedDspacingCIF', cif.CIF) +"""Reduced data in d-spacing, ready to be saved to a CIF file.""" + del sc, sciline, NewType, TypeVar diff --git a/tests/dream/geant4_reduction_test.py b/tests/dream/geant4_reduction_test.py index 4a03486..0fd0874 100644 --- a/tests/dream/geant4_reduction_test.py +++ b/tests/dream/geant4_reduction_test.py @@ -1,9 +1,13 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) +import io + import pytest import sciline import scipp as sc +import scipp.testing +from scippneutron.io.cif import Author import ess.dream.data # noqa: F401 from ess import dream, powder @@ -11,6 +15,7 @@ AccumulatedProtonCharge, BackgroundRun, CalibrationFilename, + CIFAuthors, DspacingBins, Filename, IofDspacing, @@ -20,6 +25,7 @@ NeXusSample, NeXusSource, NormalizedByProtonCharge, + ReducedDspacingCIF, SampleRun, TofMask, TwoThetaBins, @@ -29,14 +35,6 @@ WavelengthMask, ) - -@pytest.fixture -def providers(): - from ess.dream.io.geant4 import providers as geant4_providers - - return [*powder.providers, *geant4_providers] - - sample = sc.DataGroup(position=sc.vector([0.0, 0.0, 0.0], unit='mm')) source = sc.DataGroup(position=sc.vector([-3.478, 0.0, -76550], unit='mm')) charge = sc.scalar(1.0, unit='µAh') @@ -58,6 +56,13 @@ def providers(): AccumulatedProtonCharge[VanadiumRun]: charge, TwoThetaMask: None, WavelengthMask: None, + CIFAuthors: CIFAuthors( + [ + Author( + name="Jane Doe", email="jane.doe@ess.eu", orcid="0000-0000-0000-0001" + ), + ] + ), } @@ -159,3 +164,40 @@ def test_use_workflow_helper(workflow): result = workflow.compute(IofDspacing) assert result.sizes == {'dspacing': len(params[DspacingBins]) - 1} assert sc.identical(result.coords['dspacing'], params[DspacingBins]) + + +def test_pipeline_can_save_data(workflow): + workflow = powder.with_pixel_mask_filenames(workflow, []) + result = workflow.compute(ReducedDspacingCIF) + + buffer = io.StringIO() + result.save(buffer) + buffer.seek(0) + content = buffer.read() + + assert content.startswith(r'#\#CIF_1.1') + _assert_contains_source_info(content) + _assert_contains_author_info(content) + _assert_contains_beamline_info(content) + _assert_contains_dspacing_data(content) + + +def _assert_contains_source_info(cif_content: str) -> None: + assert 'diffrn_source.beamline DREAM' in cif_content + + +def _assert_contains_author_info(cif_content: str) -> None: + assert "audit_contact_author.name 'Jane Doe'" in cif_content + assert 'audit_contact_author.email jane.doe@ess.eu' in cif_content + assert 'audit_contact_author.id_orcid 0000-0000-0000-0001' in cif_content + + +def _assert_contains_beamline_info(cif_content: str) -> None: + assert 'diffrn_source.beamline DREAM' in cif_content + assert 'diffrn_source.facility ESS' in cif_content + + +def _assert_contains_dspacing_data(cif_content: str) -> None: + assert 'pd_proc.d_spacing' in cif_content + assert 'pd_proc.intensity_net' in cif_content + assert 'pd_proc.intensity_net_su' in cif_content diff --git a/tests/dream/io/cif_test.py b/tests/dream/io/cif_test.py new file mode 100644 index 0000000..bced1ee --- /dev/null +++ b/tests/dream/io/cif_test.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) + +import io + +import pytest +import scipp as sc +from scippneutron.io import cif + +import ess.dream.io.cif +from ess.powder.types import CIFAuthors, IofDspacing + + +@pytest.fixture +def iofd() -> IofDspacing: + return IofDspacing( + sc.DataArray( + sc.array(dims=['dspacing'], values=[2.1, 3.2], variances=[0.3, 0.4]), + coords={'dspacing': sc.linspace('dspacing', 0.1, 1.2, 3, unit='angstrom')}, + ) + ) + + +def save_reduced_dspacing_to_str(cif_: cif.CIF) -> str: + buffer = io.StringIO() + cif_.save(buffer) + buffer.seek(0) + return buffer.read() + + +def test_save_reduced_dspacing(iofd: IofDspacing) -> None: + from ess.dream import __version__ + + author = cif.Author(name='John Doe') + cif_ = ess.dream.io.cif.prepare_reduced_dspacing_cif( + iofd, authors=CIFAuthors([author]) + ) + result = save_reduced_dspacing_to_str(cif_) + + assert "_audit_contact_author.name 'John Doe'" in result + assert f"_computing.diffrn_reduction 'ess.dream v{__version__}'" in result + assert '_diffrn_source.beamline DREAM' in result + + loop_header = """loop_ +_pd_proc.d_spacing +_pd_proc.intensity_net +_pd_proc.intensity_net_su +""" + assert loop_header in result