From 3d9e6556670f83b05a998045ad44367a5d5db362 Mon Sep 17 00:00:00 2001 From: Philipp Schmidt Date: Mon, 20 Apr 2026 09:37:08 +0200 Subject: [PATCH 1/3] Relax restriction for RUN data to only CONTROL sources --- extra_data/sourcedata.py | 11 +++++------ extra_data/tests/test_sourcedata.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/extra_data/sourcedata.py b/extra_data/sourcedata.py index cf361511..08b93be6 100644 --- a/extra_data/sourcedata.py +++ b/extra_data/sourcedata.py @@ -473,12 +473,12 @@ def run_value(self, key, *, allow_multi_run=False): Returns the RUN parameter value corresponding to the *key* argument. """ + if not (self.is_single_run or allow_multi_run): raise MultiRunError() - if not self.is_control: - raise ValueError('Only CONTROL sources have run values, ' - f'{self.source} is a/an {self.section} source') + if self.source not in self.files[0].file['RUN']: + raise ValueError(f'{self.source} has no RUN values') # Arbitrary file - should be the same across a run ds = self.files[0].file['RUN'][self.source].get(key.replace('.', '/')) @@ -501,9 +501,8 @@ def run_values(self, inc_timestamps=True): if not self.is_single_run: raise MultiRunError() - if not self.is_control: - raise ValueError('Only CONTROL sources have run values, ' - f'{self.source} is a/an {self.section} source') + if self.source not in self.files[0].file['RUN']: + raise ValueError(f'{self.source} has no RUN values') res = {} def visitor(path, obj): diff --git a/extra_data/tests/test_sourcedata.py b/extra_data/tests/test_sourcedata.py index fc73064d..932cb30e 100644 --- a/extra_data/tests/test_sourcedata.py +++ b/extra_data/tests/test_sourcedata.py @@ -187,7 +187,7 @@ def test_run_value(mock_spb_raw_run): assert 'pulseEnergy.conversion.timestamp' not in values_dict with pytest.raises(ValueError): - # no run values for instrument sources + # no run values for AGIPD instrument data. am0.run_values() From cb534bd5a5f4821bec66237071a19b77230eaa79 Mon Sep 17 00:00:00 2001 From: Philipp Schmidt Date: Mon, 20 Apr 2026 09:37:45 +0200 Subject: [PATCH 2/3] Add SourceData.run_keys() --- extra_data/sourcedata.py | 23 +++++++++++++++++++++++ extra_data/tests/test_sourcedata.py | 3 +++ 2 files changed, 26 insertions(+) diff --git a/extra_data/sourcedata.py b/extra_data/sourcedata.py index 08b93be6..5a4a7b55 100644 --- a/extra_data/sourcedata.py +++ b/extra_data/sourcedata.py @@ -493,6 +493,29 @@ def run_value(self, key, *, allow_multi_run=False): return val.decode('utf-8', 'surrogateescape') return val + def run_keys(self, inc_timestamps=True): + """Get a set of RUN keys for this source. + + Unlike :meth:`keys`, RUN keys are not affected by selection. + """ + + if not self.is_single_run: + raise MultiRunError() + + if self.source not in self.files[0].file['RUN']: + raise ValueError(f'{self.source} has no RUN values') + + res = set() + def visitor(path, obj): + if isinstance(obj, h5py.Dataset): + res.add(path.replace('/', '.')) + + # Arbitrary file - should be the same across a run + self.files[0].file['RUN'][self.source].visititems(visitor) + if not inc_timestamps: + return {k[:-6] for k in res if k.endswith('.value')} + return res + def run_values(self, inc_timestamps=True): """Get a dict of all RUN values for this source diff --git a/extra_data/tests/test_sourcedata.py b/extra_data/tests/test_sourcedata.py index 932cb30e..025834a2 100644 --- a/extra_data/tests/test_sourcedata.py +++ b/extra_data/tests/test_sourcedata.py @@ -186,6 +186,9 @@ def test_run_value(mock_spb_raw_run): assert 'pulseEnergy.conversion' in values_dict assert 'pulseEnergy.conversion.timestamp' not in values_dict + assert xgm.run_keys() == xgm.keys() | {'classId.timestamp', 'classId.value'} + assert xgm.run_keys(False) == xgm.keys(False) | {'classId'} + with pytest.raises(ValueError): # no run values for AGIPD instrument data. am0.run_values() From 3246e9a08c81f5e78db26e904f4f06d8af38349b Mon Sep 17 00:00:00 2001 From: Philipp Schmidt Date: Mon, 20 Apr 2026 09:38:01 +0200 Subject: [PATCH 3/3] Add KeyData.run_value() --- extra_data/keydata.py | 27 ++++++++++++++++++++++++++- extra_data/sourcedata.py | 1 + extra_data/tests/test_keydata.py | 11 +++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/extra_data/keydata.py b/extra_data/keydata.py index 7bb82749..f40dc2cd 100644 --- a/extra_data/keydata.py +++ b/extra_data/keydata.py @@ -105,7 +105,7 @@ class KeyData: """ def __init__( self, source, key, *, train_ids, files, section, dtype, eshape, - inc_suspect_trains=True, + is_single_run, inc_suspect_trains=True, ): self.source = source self.key = key @@ -115,6 +115,7 @@ def __init__( self.dtype = dtype self.entry_shape = eshape self.ndim = len(eshape) + 1 + self.is_single_run = is_single_run self.inc_suspect_trains = inc_suspect_trains def _find_chunks(self): @@ -343,6 +344,7 @@ def _only_tids(self, tids, files=None): section=self.section, dtype=self.dtype, eshape=self.entry_shape, + is_single_run=self.is_single_run, inc_suspect_trains=self.inc_suspect_trains, ) @@ -472,6 +474,29 @@ def as_single_value(self, rtol=1e-5, atol=0.0, reduce_by=None): return value + def run_value(self, allow_multi_run=False): + """Get the RUN value for this key if it exists. + + This method is intended for use with data from a single run. If you + combine data from multiple runs, it will raise MultiRunError. + + Returns the RUN parameter value corresponding to this key. + """ + + from .sourcedata import SourceData # Prevent cyclic import. + + # Construct minimal SourceData object to obtain RUN value. + return SourceData( + self.source, + sel_keys=None, + train_ids=self.train_ids, + files=self.files, + section=self.section, + canonical_name=self.source, + is_single_run=self.is_single_run, + inc_suspect_trains=self.inc_suspect_trains + ).run_value(self.key, allow_multi_run=allow_multi_run) + # Getting data as different kinds of array: ------------------------------- def ndarray(self, roi=(), out=None): diff --git a/extra_data/sourcedata.py b/extra_data/sourcedata.py index 5a4a7b55..5993c705 100644 --- a/extra_data/sourcedata.py +++ b/extra_data/sourcedata.py @@ -118,6 +118,7 @@ def __getitem__(self, key): section=self.section, dtype=ds0.dtype, eshape=ds0.shape[1:], + is_single_run=self.is_single_run, inc_suspect_trains=self.inc_suspect_trains, ) diff --git a/extra_data/tests/test_keydata.py b/extra_data/tests/test_keydata.py index 52727727..ece6b6dc 100644 --- a/extra_data/tests/test_keydata.py +++ b/extra_data/tests/test_keydata.py @@ -363,6 +363,17 @@ def test_single_value(mock_sa3_control_data, monkeypatch): np.testing.assert_equal(intensity.as_single_value(rtol=1), np.median(data)) +def test_run_value(mock_sa3_control_data): + f = H5File(mock_sa3_control_data) + + flux = f['SA3_XTD10_XGM/XGM/DOOCS', 'pulseEnergy.photonFlux'] + assert flux.run_value() == 0.0 + + imager = f['SA3_XTD10_IMGFEL/CAM/BEAMVIEW:daqOutput', 'data.image.pixels'] + with pytest.raises(ValueError): + assert imager.run_value() + + def test_ndarray_out(mock_spb_raw_run): f = RunDirectory(mock_spb_raw_run) cam = f['SPB_IRU_CAM/CAM/SIDEMIC:daqOutput', 'data.image.dims']