Skip to content

Commit 42d7085

Browse files
authored
Merge pull request #623 from European-XFEL/fix/as-single-value-strings
Support and automatically convert string properties in KeyData
2 parents a4964d3 + ce5a1f9 commit 42d7085

4 files changed

Lines changed: 62 additions & 2 deletions

File tree

extra_data/keydata.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def data_counts(self, labelled=True):
309309

310310
return res
311311

312-
def as_single_value(self, rtol=1e-5, atol=0.0, reduce_by='median'):
312+
def as_single_value(self, rtol=1e-5, atol=0.0, reduce_by=None):
313313
"""Retrieve a single reduced value if within tolerances.
314314
315315
The relative and absolute tolerances *rtol* and *atol* work the
@@ -322,13 +322,33 @@ def as_single_value(self, rtol=1e-5, atol=0.0, reduce_by='median'):
322322
the first value encountered. By default, 'median' is used.
323323
324324
If within tolerances, the reduced value is returned.
325+
326+
For non-numerical keys like strings, the method instead always
327+
checks for uniqueness and returns such a value, if present.
325328
"""
326329

327330
data = self.ndarray()
328331

329332
if len(data) == 0:
330333
raise NoDataError(self.source, self.key)
331334

335+
if not np.issubdtype(self.dtype, np.number):
336+
# Handle non-numeric types first.
337+
338+
if reduce_by is not None:
339+
raise TypeError('custom reduce method not supported for '
340+
'non-numeric type')
341+
342+
unique_values = np.unique(data, axis=None)
343+
344+
if len(unique_values) > 1:
345+
raise ValueError(f'str values are not unique: {unique_values}')
346+
347+
return unique_values[0]
348+
349+
elif reduce_by is None:
350+
reduce_by = 'median'
351+
332352
if callable(reduce_by):
333353
value = reduce_by(data)
334354
elif isinstance(reduce_by, str) and hasattr(np, reduce_by):
@@ -378,6 +398,16 @@ def ndarray(self, roi=(), out=None):
378398
)
379399
dest_cursor = dest_chunk_end
380400

401+
if out.dtype.hasobject:
402+
# Can current only occur for string properties, convert from
403+
# object array of bytes to to object array of strings.
404+
# This will fail for structured dtypes containing strings,
405+
# but such are not known to us yet.
406+
out = np.array(
407+
[x.decode('utf8', 'surrogateescape') for x in out.flat],
408+
dtype=object
409+
).reshape(out.shape)
410+
381411
return out
382412

383413
def train_id_coordinates(self):

extra_data/tests/mockdata/xgm.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import numpy as np
2+
import h5py
23

34
from .base import DeviceBase
45

56
class XGM(DeviceBase):
67
control_keys = [
8+
('state', h5py.string_dtype(), ()),
79
('beamPosition/ixPos', 'f4', ()),
810
('beamPosition/iyPos', 'f4', ()),
911
('current/bottom/output', 'f4', ()),
@@ -83,3 +85,7 @@ def write_instrument(self, f):
8385
ds = grp['value']
8486
ds.attrs['alias'] = b'IX.POS'
8587
ds.attrs['daqPolicy'] = np.array([1], dtype=np.int32)
88+
89+
grp = f[f'CONTROL/{self.device_id}/state']
90+
grp['value'][:self.ntrains] = b'ON'
91+
grp['value'][:5] = b'OFF'

extra_data/tests/test_aliases.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ def test_alias_deselect(mock_sa3_control_data, mock_sa3_control_aliases_yaml):
319319
])
320320
assert subrun.all_sources == run.all_sources
321321
assert subrun.alias['sa3-xgm'].keys() == {
322+
'state.value', 'state.timestamp',
322323
'beamPosition.ixPos.value', 'beamPosition.ixPos.timestamp',
323324
'beamPosition.iyPos.value', 'beamPosition.iyPos.timestamp',
324325
'pollingInterval.value', 'pollingInterval.timestamp'}

extra_data/tests/test_keydata.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .mockdata import write_file
1212
from .mockdata.xgm import XGM
1313

14+
1415
def test_get_keydata(mock_spb_raw_run):
1516
run = RunDirectory(mock_spb_raw_run)
1617
print(run.instrument_sources)
@@ -43,6 +44,7 @@ def test_get_keydata(mock_spb_raw_run):
4344
with pytest.raises(TypeError):
4445
iter(xgm_beam_x)
4546

47+
4648
def test_select_trains(mock_spb_raw_run):
4749
run = RunDirectory(mock_spb_raw_run)
4850
xgm_beam_x = run['SPB_XTD9_XGM/DOOCS/MAIN', 'beamPosition.ixPos.value']
@@ -277,6 +279,7 @@ def test_single_value(mock_sa3_control_data, monkeypatch):
277279

278280
imager = f['SA3_XTD10_IMGFEL/CAM/BEAMVIEW:daqOutput', 'data.image.pixels']
279281
flux = f['SA3_XTD10_XGM/XGM/DOOCS', 'pulseEnergy.photonFlux']
282+
state = f['SA3_XTD10_XGM/XGM/DOOCS', 'state']
280283

281284
# Try without data for a source and key.
282285
with pytest.raises(NoDataError):
@@ -309,6 +312,17 @@ def test_single_value(mock_sa3_control_data, monkeypatch):
309312
assert flux.as_single_value(rtol=1, reduce_by=np.mean) == np.mean(data)
310313
assert flux.as_single_value(atol=len(data)-1, reduce_by='first') == 0
311314

315+
# Try strings.
316+
assert state[5:].as_single_value() == 'ON'
317+
318+
with pytest.raises(ValueError):
319+
# Contains two unique values.
320+
state.as_single_value()
321+
322+
with pytest.raises(TypeError):
323+
# Does not accept reduce_by
324+
state.as_single_value(reduce_by='mean')
325+
312326
# Try vector data.
313327
intensity = f['SA3_XTD10_XGM/XGM/DOOCS:output', 'data.intensityTD']
314328
data = np.repeat(data, intensity.shape[1]).reshape(-1, intensity.shape[-1])
@@ -332,6 +346,15 @@ def test_ndarray_out(mock_spb_raw_run):
332346
assert buf_in is buf_out
333347

334348

349+
def test_string_arrays(mock_spb_raw_run):
350+
f = RunDirectory(mock_spb_raw_run)
351+
state = f['SPB_XTD9_XGM/DOOCS/MAIN', 'state']
352+
353+
for data in [state.ndarray(), state.xarray(), state.series()]:
354+
assert data.dtype.hasobject
355+
assert (data[3:8] == ['OFF', 'OFF', 'ON', 'ON', 'ON']).all()
356+
357+
335358
def test_xarray_structured_data(mock_remi_run):
336359
run = RunDirectory(mock_remi_run)
337360
dset = run['SQS_REMI_DLD6/DET/TOP:output', 'rec.hits'].xarray()
@@ -396,6 +419,6 @@ def test_units(mock_sa3_control_data):
396419

397420
# Check that it still works after selecting 0 trains
398421
assert xgm_intensity.select_trains(np.s_[:0]).units == 'μJ'
399-
422+
400423
# units are added to xarray's attributes
401424
assert xgm_intensity.xarray().attrs['units'] == 'μJ'

0 commit comments

Comments
 (0)