Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Added:
- [Grating2DCalibration][extra.recipes.Grating2DCalibration] to calibrate data from a 2D grating detector (!284).
- Exposed detector data components from `extra_data` in `extra.components`
(AGIPD1M, AGIPD500K, DSSC1M, JUNGFRAU, LPD1M) (!177).
- Added support for passing a union of runs to
[Scantool][extra.components.Scantool] (!327).

Changed:
- [Timepix3.spatial_bins()] is now a static method.
Expand Down
43 changes: 31 additions & 12 deletions src/extra/components/scantool.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ast
import logging
from warnings import warn

from extra_data import SourceData
Expand Down Expand Up @@ -43,14 +44,34 @@ def __init__(self, run, src=None):
else:
raise RuntimeError(f"Found multiple possible scantools, please pass one explicitly with the `src` argument: {', '.join(possible_devices)}")

values = run.get_run_values(src)
self._source_name = src
self._source = run[src]

# If the run is a union then we have to use the CONTROL values rather
# than RUN values.
run_values = run.get_run_values(src) if run.is_single_run else { }
if not run.is_single_run:
logging.warning("The passed DataCollection represents multiple runs, "
"but this component will only take the Scantool settings from the first train.")

def get_value(key, raise_on_missing=True, is_str=False):
if key in run_values:
return run_values[key]
elif key in self._source:
arr = self._source[key][0].ndarray().squeeze()
return arr.item().decode() if is_str else arr
elif not raise_on_missing:
return None

raise KeyError(f"Could not find key '{key}' in either the RUN or CONTROL section")

def get_first_value(keys):
for key in keys:
if key in values:
return values[key]
x = get_value(key, raise_on_missing=False)
if x is not None:
return x

raise KeyError(f"Could not find any of these RUN section keys: {', '.join(keys)}")
raise KeyError(f"Could not find any of these keys in the RUN or CONTROL section: {', '.join(keys)}")

# These are a list of possible property names for different versions of
# the scantool. So far we've only seen the names being different, the
Expand All @@ -60,10 +81,8 @@ def get_first_value(keys):
active_motors_keys = ["deviceEnv.activeMotors.value", "activeMotors.value"]

# Get scan metadata and list of motors
self._source_name = src
self._source = run[src]
self._active = self.source["isMoving"].ndarray().any()
self._scan_type = values["scanEnv.scanType.value"]
self._scan_type = get_value("scanEnv.scanType.value", is_str=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would KeyData.as_single_value() not work here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't seem to work for strings, I get this error:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 run["FXE_DAQ_SCAN[/MDL/KARABACON](https://max-jhub.desy.de/MDL/KARABACON)", "scanEnv.scanType.value"].as_single_value()

File [~/src/EXtra-data/extra_data/keydata.py:338](https://max-jhub.desy.de/user/wrigleyj/lab/tree/notebooks/src/EXtra-data/extra_data/keydata.py#line=337), in KeyData.as_single_value(self, rtol, atol, reduce_by)
    336     value = reduce_by(data)
    337 elif isinstance(reduce_by, str) and hasattr(np, reduce_by):
--> 338     value = getattr(np, reduce_by)(data, axis=0)
    339 elif reduce_by == 'first':
    340     value = data[0]

File ~/.conda/envs/scratch/lib/python3.12/site-packages/numpy/lib/_function_base_impl.py:4001, in median(a, axis, out, overwrite_input, keepdims)
   3916 @array_function_dispatch(_median_dispatcher)
   3917 def median(a, axis=None, out=None, overwrite_input=False, keepdims=False):
   3918     """
   3919     Compute the median along the specified axis.
   3920 
   (...)   3999 
   4000     """
-> 4001     return _ureduce(a, func=_median, keepdims=keepdims, axis=axis, out=out,
   4002                     overwrite_input=overwrite_input)

File ~/.conda/envs/scratch/lib/python3.12/site-packages/numpy/lib/_function_base_impl.py:3894, in _ureduce(a, func, keepdims, **kwargs)
   3891             index_out = (0, ) * nd
   3892             kwargs['out'] = out[(Ellipsis, ) + index_out]
-> 3894 r = func(a, **kwargs)
   3896 if out is not None:
   3897     return out

File ~/.conda/envs/scratch/lib/python3.12/site-packages/numpy/lib/_function_base_impl.py:4053, in _median(a, axis, out, overwrite_input)
   4049 indexer = tuple(indexer)
   4051 # Use mean in both odd and even case to coerce data type,
   4052 # using out array if needed.
-> 4053 rout = mean(part[indexer], axis=axis, out=out)
   4054 if supports_nans and sz > 0:
   4055     # If nans are possible, warn and replace by nans like mean would.
   4056     rout = np.lib._utils_impl._median_nancheck(part, rout, axis)

File ~/.conda/envs/scratch/lib/python3.12/site-packages/numpy/_core/fromnumeric.py:3860, in mean(a, axis, dtype, out, keepdims, where)
   3857     else:
   3858         return mean(axis=axis, dtype=dtype, out=out, **kwargs)
-> 3860 return _methods._mean(a, axis=axis, dtype=dtype,
   3861                       out=out, **kwargs)

File ~[/](https://max-jhub.desy.de/).conda/envs/scratch/lib/python3.12[/](https://max-jhub.desy.de/)site-packages/numpy/_core/_methods.py:147, in _mean(a, axis, dtype, out, keepdims, where)
    145         ret = ret.dtype.type(ret / rcount)
    146 else:
--> 147     ret = ret / rcount
    149 return ret

TypeError: ufunc 'divide' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

self._motors = [x.decode() for x in get_first_value(active_motors_keys) if len(x) > 0]

# The acquisition time vector gives the length of each step, unless the
Expand All @@ -73,14 +92,14 @@ def get_first_value(keys):
# - https://git.xfel.eu/karaboDevices/Karabacon/-/blob/bd22d4a69bf7a401856f49920789ef42fda14ad2/src/karabacon/enums.py#L67
self._acquisition_time = get_first_value(acquisition_time_keys)
if _isinstance_no_import(self._acquisition_time, "numpy", "ndarray"):
if "Continuous" in values["deviceEnv.acquisitionMode.value"]:
if "Continuous" in get_value("deviceEnv.acquisitionMode.value", is_str=True):
self._acquisition_time = self._acquisition_time[0]

# The deviceEnv.activeMotors property stores the motor aliases,
# but we can try to get the actual device names from the
# actualConfiguration property.
self._motor_devices = None
motors_line = [x for x in values["actualConfiguration.value"].split("---") if "Motors:" in x]
motors_line = [x for x in get_value("actualConfiguration.value", is_str=True).split("---") if "Motors:" in x]
device_names_warning = "Couldn't extract the Karabo device names for the active motors."
if len(motors_line) == 1:
try:
Expand All @@ -95,11 +114,11 @@ def get_first_value(keys):
# Get the number of steps and start/stop positions for each motor
n_motors = len(self.motors)
self._steps = dict(zip(self.motors,
values["scanEnv.steps.value"][:n_motors]))
get_value("scanEnv.steps.value")[:n_motors]))
self._start_positions = dict(zip(self.motors,
values["scanEnv.startPoints.value"][:n_motors]))
get_value("scanEnv.startPoints.value")[:n_motors]))
self._stop_positions = dict(zip(self.motors,
values["scanEnv.stopPoints.value"][:n_motors]))
get_value("scanEnv.stopPoints.value")[:n_motors]))

@property
def source_name(self) -> str:
Expand Down
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .mockdata.timepix import Timepix3Receiver, Timepix3Centroids
from .mockdata.timeserver import PulsePatternDecoder, Timeserver
from .mockdata.xgm import XGM, XGMD, XGMReduced
from .mockdata.karabacon import Karabacon


@pytest.fixture(scope='session')
Expand Down Expand Up @@ -156,3 +157,13 @@ def mock_timepix_exceeded_buffer_run(mock_sqs_timepix_directory):
size_dset[np.argmax(size_dset)] += tpx_root['data/x'].shape[1]

yield RunDirectory(td).deselect('SQS_EXTRA*')

@pytest.fixture(scope="function")
def mock_scantool_run():
sources = [
Karabacon("FXE_DAQ_SCAN/MDL/KARABACON"),
]

with TemporaryDirectory() as td:
write_file(Path(td) / 'RAW-R0001-DA01-S00000.h5', sources, 100)
yield RunDirectory(td)
42 changes: 42 additions & 0 deletions tests/mockdata/karabacon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import h5py
import numpy as np
from extra_data.tests.mockdata.base import DeviceBase

str_dtype = h5py.special_dtype(vlen=str)

class Karabacon(DeviceBase):
control_keys = [
("isMoving", "uint8", ()),
("deviceEnv/acquisitionMode", str_dtype, (1,)),
("deviceEnv/acquisitionTimes", "f8", (10_000,)),
("deviceEnv/activeMotors", str_dtype, (1000,)),
("scanEnv/scanType", str_dtype, (1,)),
("scanEnv/steps", "int32", (6,)),
("scanEnv/startPoints", "f8", (6,)),
("scanEnv/stopPoints", "f8", (6,)),
("actualConfiguration", str_dtype, (1,))
]

def write_control(self, f):
super().write_control(f)

# Values taken from p7948, run 154
mock_values = {
"deviceEnv/acquisitionMode": np.array([b'Continuous Averaged'], dtype=object),
"deviceEnv/acquisitionTimes": np.insert(np.ones(9999), 0, 30),
"deviceEnv/activeMotors": np.array([b"ppl_odl"] + [b""] * 999, dtype=object),
"scanEnv/scanType": np.array([b'dscan'], dtype=object),
"scanEnv/steps": np.array([20, 1, 1, 1, 1, 1], dtype=np.int32),
"scanEnv/startPoints": np.array([-15., 1., 1., 1., 1., 1.]),
"scanEnv/stopPoints": np.array([25., 1., 1., 1., 1., 1.]),
"actualConfiguration": np.array([b"--- Motors: ['FXE_AUXT_LIC/DOOCS/PPODL:default']--- Data Sources: ['FXE_EXP_ONC/METRO/USER_XAS:output0.schema.data.value', 'FXE_EXP_ONC/METRO/USER_XAS:output1.schema.data.value', 'FXE_EXP_ONC/METRO/USER_XAS:output2.schema.data.value', 'FXE_EXP_ONC/METRO/USER_XAS:output6.schema.data.value', 'FXE_EXP_ONC/METRO/USER_XAS:output7.schema.data.value', 'FXE_EXP_ONC/METRO/USER_XAS:output8.schema.data.value']--- Triggers: []"],
dtype=object)
}


for key, value in mock_values.items():
ds_key = f"CONTROL/{self.device_id}/{key}/value"
ds = f[ds_key]
ntrains = ds.shape[0]
for i in range(ntrains):
ds[i, :] = value
15 changes: 14 additions & 1 deletion tests/test_components_scantool.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import logging
from unittest.mock import MagicMock

from extra.components import Scantool

import pytest
import numpy as np

def test_scantool():
def test_scantool(mock_scantool_run, mock_spb_aux_run, caplog):
mock_name = "CRISPY/KARABACON"
mock_source = MagicMock()

Expand Down Expand Up @@ -77,6 +78,18 @@ def test_scantool():
scantool = Scantool(mock_run)
assert scantool.acquisition_time == 20

# Test creating a Scantool from a union of two runs with only control data,
# see mockdata/karabacon.py for the settings it was created with.
with caplog.at_level(logging.WARNING):
scantool = Scantool(mock_scantool_run.union(mock_spb_aux_run))
assert len(caplog.records) == 1

assert scantool.scan_type == "dscan"
assert scantool.motors == ["ppl_odl"]
assert scantool.acquisition_time == 30
assert scantool.start_positions == { "ppl_odl": -15 }
assert scantool.stop_positions == { "ppl_odl": 25 }

# Smoke tests
scantool.info()
scantool.format()
Expand Down