Skip to content

Commit 729f16e

Browse files
authored
Merge pull request #119 from scipp/result-data
Add data property on results that returns a DataGroup
2 parents f4d8549 + 3badc1d commit 729f16e

4 files changed

Lines changed: 67 additions & 55 deletions

File tree

src/tof/model.py

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,6 @@
1616
ComponentType = Chopper | Detector
1717

1818

19-
def _input_to_dict(
20-
obj: None | list[ComponentType] | tuple[ComponentType, ...] | ComponentType,
21-
kind: type,
22-
):
23-
if isinstance(obj, list | tuple):
24-
out = {}
25-
for item in obj:
26-
new = _input_to_dict(item, kind=kind)
27-
for key in new.keys():
28-
if key in out:
29-
raise ValueError(f"More than one component named '{key}' found.")
30-
out.update(new)
31-
return out
32-
elif isinstance(obj, kind):
33-
return {obj.name: obj}
34-
elif obj is None:
35-
return {}
36-
else:
37-
raise TypeError(
38-
"Invalid input type. Must be a Chopper or a Detector, "
39-
"or a list/tuple of Choppers or Detectors."
40-
)
41-
42-
4319
def _array_or_none(container: dict, key: str) -> sc.Variable | None:
4420
return (
4521
sc.array(
@@ -131,12 +107,20 @@ class Model:
131107
def __init__(
132108
self,
133109
source: Source | None = None,
134-
choppers: Chopper | list[Chopper] | tuple[Chopper, ...] | None = None,
135-
detectors: Detector | list[Detector] | tuple[Detector, ...] | None = None,
110+
choppers: list[Chopper] | tuple[Chopper, ...] | None = None,
111+
detectors: list[Detector] | tuple[Detector, ...] | None = None,
136112
):
137-
self.choppers = _input_to_dict(choppers, kind=Chopper)
138-
self.detectors = _input_to_dict(detectors, kind=Detector)
113+
self.choppers = {}
114+
self.detectors = {}
139115
self.source = source
116+
for components, kind in ((choppers, Chopper), (detectors, Detector)):
117+
for c in components or ():
118+
if not isinstance(c, kind):
119+
raise TypeError(
120+
f"Beamline components: expected {kind.__name__} instance, "
121+
f"got {type(c)}."
122+
)
123+
self.add(c)
140124

141125
@classmethod
142126
def from_json(cls, filename: str) -> Model:
@@ -212,31 +196,33 @@ def to_json(self, filename: str):
212196
with open(filename, 'w') as f:
213197
json.dump(self.as_json(), f, indent=2)
214198

215-
def add(self, component):
199+
def add(self, component: Chopper | Detector):
216200
"""
217201
Add a component to the instrument.
218202
Component names must be unique across choppers and detectors.
203+
The name "source" is reserved for the source, and can thus not be used for other
204+
components.
219205
220206
Parameters
221207
----------
222208
component:
223209
A chopper or detector.
224210
"""
225-
if component.name in chain(self.choppers, self.detectors):
211+
if not isinstance(component, (Chopper | Detector)):
212+
raise TypeError(
213+
f"Cannot add component of type {type(component)} to the model. "
214+
"Only Chopper and Detector instances are allowed."
215+
)
216+
# Note that the name "source" is reserved for the source.
217+
if component.name in chain(self.choppers, self.detectors, ("source",)):
226218
raise KeyError(
227219
f"Component with name {component.name} already exists. "
228220
"If you wish to replace/update an existing component, use "
229221
"``model.choppers['name'] = new_chopper`` or "
230222
"``model.detectors['name'] = new_detector``."
231223
)
232-
if isinstance(component, Chopper):
233-
self.choppers[component.name] = component
234-
elif isinstance(component, Detector):
235-
self.detectors[component.name] = component
236-
else:
237-
raise TypeError(
238-
f"Cannot add component of type {type(component)} to the model."
239-
)
224+
container = self.choppers if isinstance(component, Chopper) else self.detectors
225+
container[component.name] = component
240226

241227
def remove(self, name: str):
242228
"""

src/tof/result.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,18 @@ def to_nxevent_data(self, key: str | None = None) -> sc.DataArray:
344344
)
345345
out.coords["Ltotal"] = out.coords.pop("distance")
346346
return out
347+
348+
@property
349+
def data(self) -> sc.DataGroup:
350+
"""
351+
Get the data for the source, choppers, and detectors, as a DataGroup.
352+
The components are sorted by distance.
353+
"""
354+
out = {"source": self.source.data}
355+
components = sorted(
356+
chain(self.choppers.values(), self.detectors.values()),
357+
key=lambda c: c.distance.value,
358+
)
359+
for comp in components:
360+
out[comp.name] = comp.data
361+
return sc.DataGroup(out)

src/tof/source.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,14 @@ def data(self) -> sc.DataArray:
290290
"""
291291
The data array containing the neutrons in the pulse.
292292
"""
293-
return self._data
293+
return self._data.assign_coords(
294+
{
295+
"distance": self._distance,
296+
"eto": self._data.coords["birth_time"]
297+
% (1.0 / self._frequency).to(unit=TIME_UNIT, copy=False),
298+
"toa": self._data.coords["birth_time"],
299+
}
300+
)
294301

295302
@classmethod
296303
def from_neutrons(

tests/model_test.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -382,11 +382,11 @@ def test_create_model_with_duplicate_component_names_raises(
382382
chopper = dummy_chopper
383383
detector = dummy_detector
384384
with pytest.raises(
385-
ValueError, match="More than one component named 'dummy_chopper' found"
385+
KeyError, match="Component with name dummy_chopper already exists"
386386
):
387387
tof.Model(source=dummy_source, choppers=[chopper, chopper])
388388
with pytest.raises(
389-
ValueError, match="More than one component named 'dummy_detector' found"
389+
KeyError, match="Component with name dummy_detector already exists"
390390
):
391391
tof.Model(source=dummy_source, detectors=[detector, detector])
392392

@@ -422,28 +422,32 @@ def test_getitem(dummy_chopper, dummy_detector, dummy_source):
422422
model['foo']
423423

424424

425-
def test_input_can_be_single_component(dummy_chopper, dummy_detector, dummy_source):
426-
chopper = dummy_chopper
427-
detector = dummy_detector
428-
model = tof.Model(source=dummy_source, choppers=chopper, detectors=detector)
429-
assert 'dummy_chopper' in model.choppers
430-
assert 'dummy_detector' in model.detectors
431-
432-
433425
def test_bad_input_type_raises(dummy_chopper, dummy_detector, dummy_source):
434426
chopper = dummy_chopper
435427
detector = dummy_detector
436-
with pytest.raises(TypeError, match='Invalid input type'):
428+
with pytest.raises(
429+
TypeError, match='Beamline components: expected Chopper instance'
430+
):
437431
_ = tof.Model(source=dummy_source, choppers='bad chopper')
438-
with pytest.raises(TypeError, match='Invalid input type'):
432+
with pytest.raises(
433+
TypeError, match='Beamline components: expected Detector instance'
434+
):
439435
_ = tof.Model(source=dummy_source, choppers=[chopper], detectors='abc')
440-
with pytest.raises(TypeError, match='Invalid input type'):
436+
with pytest.raises(
437+
TypeError, match='Beamline components: expected Chopper instance'
438+
):
441439
_ = tof.Model(source=dummy_source, choppers=[chopper, 'bad chopper'])
442-
with pytest.raises(TypeError, match='Invalid input type'):
440+
with pytest.raises(
441+
TypeError, match='Beamline components: expected Detector instance'
442+
):
443443
_ = tof.Model(source=dummy_source, detectors=(1234, detector))
444-
with pytest.raises(TypeError, match='Invalid input type'):
444+
with pytest.raises(
445+
TypeError, match='Beamline components: expected Chopper instance'
446+
):
445447
_ = tof.Model(source=dummy_source, choppers=[detector])
446-
with pytest.raises(TypeError, match='Invalid input type'):
448+
with pytest.raises(
449+
TypeError, match='Beamline components: expected Detector instance'
450+
):
447451
_ = tof.Model(source=dummy_source, detectors=[chopper])
448452

449453

0 commit comments

Comments
 (0)