diff --git a/packages/essimaging/docs/odin/index.md b/packages/essimaging/docs/odin/index.md index 9cbf1c8e6..0eb03e600 100644 --- a/packages/essimaging/docs/odin/index.md +++ b/packages/essimaging/docs/odin/index.md @@ -6,5 +6,6 @@ maxdepth: 1 --- odin-data-reduction +odin-orca-image-normalization odin-make-wavelength-lookup-table ``` diff --git a/packages/essimaging/docs/odin/odin-orca-image-normalization.ipynb b/packages/essimaging/docs/odin/odin-orca-image-normalization.ipynb new file mode 100644 index 000000000..74961299e --- /dev/null +++ b/packages/essimaging/docs/odin/odin-orca-image-normalization.ipynb @@ -0,0 +1,179 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Odin: Orca image normalization workflow\n", + "\n", + "This notebook shows how to use the workflow to compute normalized images recorded by the Orca detector on the ODIN instrument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import ess.odin.data # noqa: F401\n", + "from ess import odin\n", + "from ess.imaging.types import *\n", + "import scipp as sc\n", + "import plopp as pp\n", + "\n", + "%matplotlib widget" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Workflow setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "wf = odin.OdinOrcaWorkflow()\n", + "\n", + "wf[Filename[AllRuns]] = odin.data.odin_lego_images()\n", + "\n", + "wf[NeXusDetectorName] = 'histogram_mode_detectors/orca'\n", + "\n", + "wf[MaskingRules] = {} # No masks to begin with\n", + "wf[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "wf.visualize(NormalizedImage, compact=True, graph_attr={\"rankdir\": \"LR\"})" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Run the workflow\n", + "\n", + "We compute the final normalized image:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "image = wf.compute(NormalizedImage)\n", + "image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "pp.slicer(image, autoscale=False)" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Adding masks\n", + "\n", + "If we want to mask some part of the image, we update the masking rules.\n", + "For example, here we mask the upper part of the image:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "wf[MaskingRules] = {'y_pixel_offset': lambda x: x > sc.scalar(0.082, unit='m')}\n", + "\n", + "pp.slicer(wf.compute(NormalizedImage), autoscale=False)" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Intermediate results\n", + "\n", + "We can also inspect intermediate results, which is useful for debugging:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "results = wf.compute(\n", + " [\n", + " FluxNormalizedDetector[SampleRun],\n", + " FluxNormalizedDetector[OpenBeamRun],\n", + " BackgroundSubtractedDetector[SampleRun],\n", + " ]\n", + ")\n", + "\n", + "fig = pp.tiled(2, 2, hspace=0.3, wspace=0.3)\n", + "fig[0, 0] = results[FluxNormalizedDetector[SampleRun]]['time', 0].plot(\n", + " title='Sample (proton-charge normalized)'\n", + ")\n", + "fig[0, 1] = results[FluxNormalizedDetector[OpenBeamRun]]['time', 0].plot(\n", + " title='Open beam (proton-charge normalized)'\n", + ")\n", + "fig[1, 0] = results[BackgroundSubtractedDetector[SampleRun]]['time', 0].plot(\n", + " title='Background subtracted sample'\n", + ")\n", + "fig[1, 1] = image['time', 0].plot(title='Final image')\n", + "fig" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/essimaging/src/ess/tbl/orca.py b/packages/essimaging/src/ess/imaging/orca.py similarity index 81% rename from packages/essimaging/src/ess/tbl/orca.py rename to packages/essimaging/src/ess/imaging/orca.py index 2ac116369..60adef2ad 100644 --- a/packages/essimaging/src/ess/tbl/orca.py +++ b/packages/essimaging/src/ess/imaging/orca.py @@ -8,13 +8,12 @@ import scipp as sc from ess.reduce.nexus import GenericNeXusWorkflow -from ess.reduce.nexus.types import NeXusDetectorName, NeXusName from .. import imaging from ..imaging.types import ( + AllRuns, CorrectedDetector, DarkBackgroundRun, - ExposureTime, FluxNormalizedDetector, OpenBeamRun, ProtonCharge, @@ -58,21 +57,13 @@ def normalize_by_proton_charge_orca( orca_providers = (normalize_by_proton_charge_orca,) -def default_parameters() -> dict: - """Return the default NeXus names and detector name for the ORCA workflow.""" - return { - NeXusDetectorName: 'orca_detector', - NeXusName[ExposureTime]: '/entry/instrument/orca_detector/camera_exposure', - } - - def OrcaNormalizedImagesWorkflow(**kwargs) -> sl.Pipeline: """ Workflow with default parameters for ORCA image normalization. """ wf = GenericNeXusWorkflow( - run_types=[SampleRun, OpenBeamRun, DarkBackgroundRun], + run_types=[SampleRun, OpenBeamRun, DarkBackgroundRun, AllRuns], monitor_types=[], **kwargs, ) @@ -83,6 +74,4 @@ def OrcaNormalizedImagesWorkflow(**kwargs) -> sl.Pipeline: *orca_providers, ): wf.insert(provider) - for key, param in default_parameters().items(): - wf[key] = param return wf diff --git a/packages/essimaging/src/ess/imaging/types.py b/packages/essimaging/src/ess/imaging/types.py index 21fe425f5..b8a37cfbf 100644 --- a/packages/essimaging/src/ess/imaging/types.py +++ b/packages/essimaging/src/ess/imaging/types.py @@ -36,7 +36,6 @@ UncertaintyBroadcastMode = _UncertaintyBroadcastMode - SampleRun = NewType("SampleRun", int) """Sample run; a run with a sample in the beam.""" @@ -48,6 +47,9 @@ """Open beam run; a run with no sample in the beam, and the shutter open, to measure the beam profile.""" +AllRuns = NewType("AllRuns", int) +"""A type to represent all runs: sample, open beam, and dark background runs.""" + BeamMonitor1 = NewType("BeamMonitor1", int) """Beam monitor number 1""" @@ -100,4 +102,12 @@ class ExposureTime(sciline.Scope[RunType, sc.DataArray], sc.DataArray): """Exposure time of each frame recorded by the camera detector.""" +ImageKey = NewType("ImageKey", sc.DataArray) +"""Key mapping image type to the time axis of the recording: + - 0: sample run + - 1: open beam run + - 2: dark background run + """ + + del sc, sciline, NewType diff --git a/packages/essimaging/src/ess/odin/__init__.py b/packages/essimaging/src/ess/odin/__init__.py index 5a5630d9c..28d0831ba 100644 --- a/packages/essimaging/src/ess/odin/__init__.py +++ b/packages/essimaging/src/ess/odin/__init__.py @@ -4,7 +4,7 @@ import importlib.metadata from . import beamline -from .workflows import OdinBraggEdgeWorkflow, OdinWorkflow +from .workflows import OdinBraggEdgeWorkflow, OdinOrcaWorkflow, OdinWorkflow try: __version__ = importlib.metadata.version("esstbl") @@ -15,6 +15,7 @@ __all__ = [ "OdinBraggEdgeWorkflow", + "OdinOrcaWorkflow", "OdinWorkflow", "beamline", ] diff --git a/packages/essimaging/src/ess/odin/data.py b/packages/essimaging/src/ess/odin/data.py index c92effcc9..4d980e903 100644 --- a/packages/essimaging/src/ess/odin/data.py +++ b/packages/essimaging/src/ess/odin/data.py @@ -15,6 +15,7 @@ "iron_simulation_sample_large.nxs": "md5:c162b6abeccb51984880d8d5002bae95", "iron_simulation_sample_small.nxs": "md5:dda6fb30aa88780c5a3d4cef6ea05278", "ODIN-wavelength-lookup-table-5m-65m.h5": "md5:44eef2a2e826cec688aeb1b985eb9f9e", # noqa: E501 + "ymir_lego_odin.hdf": "md5:8e8708891e2574046b6f372e5e3516a5", }, ) @@ -69,3 +70,13 @@ def odin_wavelength_lookup_table() -> pathlib.Path: with ``NumberOfSimulatedNeutrons = 5_000_000``. """ return _registry.get_path("ODIN-wavelength-lookup-table-5m-65m.h5") + + +def odin_lego_images() -> pathlib.Path: + """ + Return the path to the ODIN LEGO HDF5 file, created from the YMIR data. + This file was created using the tools/make-odin-images-from-ymir.ipynb notebook. + A ODIN file (coda_odin_999999_00011093.hdf) was used as a template for the NeXus + structure. The images were extracted from the YMIR LEGO run. + """ + return _registry.get_path("ymir_lego_odin.hdf") diff --git a/packages/essimaging/src/ess/odin/workflows.py b/packages/essimaging/src/ess/odin/workflows.py index e52086d9c..d4a6e71be 100644 --- a/packages/essimaging/src/ess/odin/workflows.py +++ b/packages/essimaging/src/ess/odin/workflows.py @@ -5,17 +5,24 @@ """ import sciline +import scipp as sc from scippneutron.conversion.tof import tof_from_wavelength +from ess.reduce.nexus import load_from_path +from ess.reduce.nexus.types import NeXusFileSpec, NeXusLocationSpec, NeXusName from ess.reduce.unwrap import GenericUnwrapWorkflow, WavelengthLutMode +from ..imaging import orca from ..imaging.types import ( + AllRuns, BeamMonitor1, BeamMonitor2, BeamMonitor3, BeamMonitor4, CorrectedDetector, DarkBackgroundRun, + FluxNormalizedDetector, + ImageKey, LookupTableRelativeErrorThreshold, NeXusMonitorName, OpenBeamRun, @@ -104,7 +111,78 @@ def OdinBraggEdgeWorkflow( return workflow +def load_image_key(file: NeXusFileSpec[AllRuns], path: NeXusName[ImageKey]) -> ImageKey: + # Note that putting '/value' at the end of the 'path' in the default_parameters + # yields different results as it can return a Variable instead of a DataArray, + # depending on the contents of the NeXus file. + return ImageKey( + load_from_path(NeXusLocationSpec(filename=file.value, component_name=path))[ + "value" + ] + ) + + +KEY_MAPPING = { + SampleRun: sc.scalar(0, unit=None), + OpenBeamRun: sc.scalar(1, unit=None), + DarkBackgroundRun: sc.scalar(2, unit=None), +} + + +def _extract_part_of_run( + data: sc.DataArray, image_key: ImageKey, run_type: RunType +) -> sc.DataArray: + """ """ + key_lookup = sc.lookup(image_key, "time", mode="previous") + sel = key_lookup[data.coords["time"]] == KEY_MAPPING[run_type] + return data[sel] + + +def extract_dark_run( + data: FluxNormalizedDetector[AllRuns], image_key: ImageKey +) -> FluxNormalizedDetector[DarkBackgroundRun]: + """ """ + return FluxNormalizedDetector[DarkBackgroundRun]( + _extract_part_of_run(data=data, image_key=image_key, run_type=DarkBackgroundRun) + ) + + +def extract_openbeam_run( + data: FluxNormalizedDetector[AllRuns], image_key: ImageKey +) -> FluxNormalizedDetector[OpenBeamRun]: + """ """ + return FluxNormalizedDetector[OpenBeamRun]( + _extract_part_of_run(data=data, image_key=image_key, run_type=OpenBeamRun) + ) + + +def extract_sample_run( + data: FluxNormalizedDetector[AllRuns], image_key: ImageKey +) -> FluxNormalizedDetector[SampleRun]: + """ """ + return FluxNormalizedDetector[SampleRun]( + _extract_part_of_run(data=data, image_key=image_key, run_type=SampleRun) + ) + + +def OdinOrcaWorkflow(**kwargs) -> sciline.Pipeline: + """ """ + wf = orca.OrcaNormalizedImagesWorkflow(**kwargs) + for provider in ( + load_image_key, + extract_dark_run, + extract_openbeam_run, + extract_sample_run, + ): + wf.insert(provider) + wf[NeXusName[ImageKey]] = ( + '/entry/instrument/histogram_mode_detectors/orca/image_key' + ) + return wf + + __all__ = [ "OdinBraggEdgeWorkflow", + "OdinOrcaWorkflow", "OdinWorkflow", ] diff --git a/packages/essimaging/src/ess/tbl/__init__.py b/packages/essimaging/src/ess/tbl/__init__.py index 8476e94db..b157c3e8e 100644 --- a/packages/essimaging/src/ess/tbl/__init__.py +++ b/packages/essimaging/src/ess/tbl/__init__.py @@ -3,9 +3,8 @@ import importlib.metadata -from . import orca -from .orca import OrcaNormalizedImagesWorkflow -from .workflow import TblWorkflow, default_parameters +from ..imaging import orca +from .workflow import OrcaNormalizedImagesWorkflow, TblWorkflow, default_parameters try: __version__ = importlib.metadata.version("esstbl") diff --git a/packages/essimaging/src/ess/tbl/workflow.py b/packages/essimaging/src/ess/tbl/workflow.py index 5c1cf6d5d..93f023eeb 100644 --- a/packages/essimaging/src/ess/tbl/workflow.py +++ b/packages/essimaging/src/ess/tbl/workflow.py @@ -6,8 +6,10 @@ import sciline +from ess.reduce.nexus.types import NeXusDetectorName from ess.reduce.unwrap import GenericUnwrapWorkflow, WavelengthLutMode +from ..imaging.orca import OrcaNormalizedImagesWorkflow as _OrcaWorkflow from ..imaging.types import ( BeamMonitor1, LookupTableRelativeErrorThreshold, @@ -57,3 +59,12 @@ def TblWorkflow( for key, param in default_parameters().items(): workflow[key] = param return workflow + + +def OrcaNormalizedImagesWorkflow(**kwargs) -> sciline.Pipeline: + """ + Workflow with default parameters for ORCA image normalization. + """ + wf = _OrcaWorkflow(**kwargs) + wf[NeXusDetectorName] = 'orca_detector' + return wf diff --git a/packages/essimaging/tests/odin/orca_workflow_test.py b/packages/essimaging/tests/odin/orca_workflow_test.py new file mode 100644 index 000000000..3b25406a0 --- /dev/null +++ b/packages/essimaging/tests/odin/orca_workflow_test.py @@ -0,0 +1,119 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Scipp contributors (https://github.com/scipp) + +import ess.odin.data # noqa: F401 +import pytest +import sciline as sl +import scipp as sc +from ess import odin +from scipp.testing import assert_identical + +from ess.imaging.types import ( + AllRuns, + BackgroundSubtractedDetector, + CorrectedDetector, + DarkBackgroundRun, + Filename, + FluxNormalizedDetector, + ImageKey, + MaskingRules, + MeanDarkFrame, + NeXusDetectorName, + NormalizedImage, + OpenBeamRun, + ProtonCharge, + RawDetector, + SampleRun, + UncertaintyBroadcastMode, +) + + +@pytest.fixture +def workflow() -> sl.Pipeline: + """ + Workflow for normalizing ODIN Orca images. + """ + + wf = odin.OdinOrcaWorkflow() + wf[Filename[AllRuns]] = odin.data.odin_lego_images() + wf[MaskingRules] = {} + wf[NeXusDetectorName] = 'histogram_mode_detectors/orca' + wf[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + return wf + + +def test_workflow_loads_raw_data(workflow): + da = workflow.compute(RawDetector[AllRuns]) + assert "position" in da.coords + assert "time" in da.coords + assert da.ndim == 3 + assert "time" in da.dims + + +def test_workflow_loads_proton_charge(workflow): + pc = workflow.compute(ProtonCharge[AllRuns]) + assert "time" in pc.coords + assert "time" in pc.dims + assert pc.unit == "uC" + + +def test_workflow_applies_masks(workflow): + workflow[MaskingRules] = { + 'y_pixel_offset': lambda x: x > sc.scalar(0.082, unit='m') + } + da = workflow.compute(RawDetector[AllRuns]) + masked_da = workflow.compute(CorrectedDetector[AllRuns]) + assert 'y_pixel_offset' in masked_da.masks + assert da.sum().value > masked_da.sum().value + + +def test_workflow_normalizes_by_proton_charge(workflow): + da = workflow.compute(FluxNormalizedDetector[AllRuns]) + assert da.ndim == 3 + assert "time" in da.dims + # TODO: should it be "counts / uC"? + assert da.unit == "1 / uC" + + +def test_workflow_computes_image_key(workflow): + image_key = workflow.compute(ImageKey) + assert image_key.ndim == 1 + assert "time" in image_key.dims + assert image_key.unit is None + + +@pytest.mark.parametrize("run", [OpenBeamRun, DarkBackgroundRun, SampleRun]) +def test_workflow_extracts_different_runs_according_to_image_key(workflow, run): + da = workflow.compute(FluxNormalizedDetector[run]) + assert da.ndim == 3 + assert "time" in da.dims + # Data should be normalized by proton charge, so unit should be "1 / uC" + assert da.unit == "1 / uC" + + +def test_workflow_computes_mean_dark_frame(workflow): + dark_frames = workflow.compute(FluxNormalizedDetector[DarkBackgroundRun]) + mean_dark_frame = workflow.compute(MeanDarkFrame) + assert mean_dark_frame.ndim == 2 + assert "time" not in mean_dark_frame.dims + assert mean_dark_frame.unit == dark_frames.unit + + +@pytest.mark.parametrize("run", [OpenBeamRun, SampleRun]) +def test_workflow_subtracts_dark_background(workflow, run): + background = workflow.compute(MeanDarkFrame) + before = workflow.compute(FluxNormalizedDetector[run]) + after = workflow.compute(BackgroundSubtractedDetector[run]) + + assert_identical(sc.values(after), sc.values(before) - sc.values(background)) + + +def test_workflow_computes_normalized_image(workflow): + sample = workflow.compute(BackgroundSubtractedDetector[SampleRun]) + open_beam = workflow.compute(BackgroundSubtractedDetector[OpenBeamRun]) + normalized = workflow.compute(NormalizedImage) + + assert_identical( + sc.values(normalized), sc.values(sample) / sc.values(open_beam.mean('time')) + ) + assert normalized.unit == sc.units.dimensionless diff --git a/packages/essimaging/tools/make-odin-data-from-ymir.ipynb b/packages/essimaging/tools/make-odin-data-from-ymir.ipynb new file mode 100644 index 000000000..c3445de64 --- /dev/null +++ b/packages/essimaging/tools/make-odin-data-from-ymir.ipynb @@ -0,0 +1,227 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Make Odin dark, open beam and sample data from Ymir data\n", + "\n", + "This notebook creates a file for Odin that contain images in the Orca detector file entry.\n", + "We use a ODIN file written by ECDC as a template and replace the orca detector data with images recorded at Ymir.\n", + "\n", + "The Ymir data was a single file containing dark frame, open beam, and sample data,\n", + "with a flag indicating which frame is part of which recording.\n", + "\n", + "We also generate a fake proton-charge log and place it inside the `neutron_prod_info` group.\n", + "The proton charge is a time series from the first frame time to the last frame time + exposure time, where the value is randomly oscillating around 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import h5py as h5\n", + "import numpy as np\n", + "import scipp as sc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ess.ymir.data import ymir_lego_images_path" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load Ymir data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "path = 'entry/instrument/histogram_mode_detectors/orca/'\n", + "\n", + "with h5.File(ymir_lego_images_path()) as f:\n", + " lego_data = f[path + \"data/value\"][()]\n", + " lego_time = f[path + 'data/time'][()]\n", + " imkey = f[path + 'image_key']\n", + " image_key = {\n", + " k: {'values': imkey[k][()], 'attrs': dict(imkey[k].attrs)} for k in imkey.keys()\n", + " }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create fake proton charge" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "exposure_time = int(5e8) # 0.5s (made up)\n", + "period = 1e9 / 14\n", + "\n", + "# Need to start a bit before the start of the time axis of the data\n", + "# so that sc.lookup in 'previous' mode can find the pulse time\n", + "charge_time = np.arange(\n", + " float(lego_time[0]) - exposure_time, float(lego_time[-1] + exposure_time), period\n", + ").astype('uint64')\n", + "charge_value = np.random.uniform(0.99, 1.01, size=len(charge_time)).astype('float64')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Make pixel offsets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "npix = lego_data.shape[-1]\n", + "\n", + "x_pixel_offset = sc.linspace('dim_1', -0.1, 0.1, npix, unit='m')\n", + "y_pixel_offset = sc.linspace('dim_2', -0.1, 0.1, npix, unit='m')\n", + "x_pixel_offset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create new files from template" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def replace_dataset(entry, name, values):\n", + " attrs = dict(entry[name].attrs)\n", + " del entry[name]\n", + " dset = entry.create_dataset(name, data=values)\n", + " dset.attrs.update(attrs)\n", + "\n", + "\n", + "def make_new_file(template_nexus_file, outfile, data, proton_charge, image_key):\n", + " import shutil\n", + "\n", + " shutil.copyfile(template_nexus_file, outfile)\n", + "\n", + " instr_path = 'entry/instrument/'\n", + "\n", + " detector_path = instr_path + 'histogram_mode_detectors/orca/' # ODIN\n", + " # detector_path = instr_path + 'orca_detector/' # TBL\n", + " proton_charge_path = 'entry/neutron_prod_info/pulse_charge'\n", + "\n", + " with h5.File(outfile, \"r+\") as f:\n", + " detector_data = f[detector_path + \"data\"]\n", + " replace_dataset(detector_data, name=\"value\", values=data[\"value\"])\n", + " replace_dataset(detector_data, name=\"time\", values=data[\"time\"])\n", + " detector_data.attrs['axes'] = ['time', 'y_pixel_offset', 'x_pixel_offset']\n", + "\n", + " detector = f[detector_path]\n", + "\n", + " imkey_attrs = dict(detector['image_key'].attrs)\n", + " del detector['image_key']\n", + " g = detector.create_group(\"image_key\")\n", + " g.attrs.update(imkey_attrs)\n", + " for key, value in image_key.items():\n", + " new = detector['image_key'].create_dataset(key, data=value['values'])\n", + " new.attrs.update(value['attrs'])\n", + "\n", + " del detector['x_pixel_offset']\n", + " xoff = detector.create_dataset('x_pixel_offset', data=x_pixel_offset.values)\n", + " xoff.attrs['units'] = str(x_pixel_offset.unit)\n", + " xoff.attrs['axis'] = 1\n", + " del detector['y_pixel_offset']\n", + " yoff = detector.create_dataset('y_pixel_offset', data=y_pixel_offset.values)\n", + " yoff.attrs['units'] = str(y_pixel_offset.unit)\n", + " yoff.attrs['axis'] = 2\n", + "\n", + " pcharge = f[proton_charge_path]\n", + " replace_dataset(pcharge, name=\"value\", values=proton_charge[\"value\"])\n", + " replace_dataset(pcharge, name=\"time\", values=proton_charge[\"time\"])\n", + " replace_dataset(\n", + " pcharge, name=\"average_value\", values=proton_charge[\"value\"].mean()\n", + " )\n", + " replace_dataset(\n", + " pcharge, name=\"minimum_value\", values=proton_charge[\"value\"].min()\n", + " )\n", + " replace_dataset(\n", + " pcharge, name=\"maximum_value\", values=proton_charge[\"value\"].max()\n", + " )\n", + "\n", + " del f[instr_path + 'event_mode_detectors']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "template_nexus_file = \"coda_odin_999999_00011093.hdf\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Remember to 'h5repack' the new file after it is created\n", + "\n", + "make_new_file(\n", + " template_nexus_file=template_nexus_file,\n", + " outfile=\"ymir_lego_odin.hdf\",\n", + " data={\"value\": lego_data, \"time\": lego_time},\n", + " proton_charge={\"value\": charge_value, \"time\": charge_time},\n", + " image_key=image_key,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}