From 855c23b0eeee34101c34feb7baf42176c7f6bf14 Mon Sep 17 00:00:00 2001 From: Sinq User Console Account Date: Fri, 29 May 2026 10:22:39 +0200 Subject: [PATCH] J. Stahn - added flatbuffer scheme 'da00' for the bema monitor - reading in beam monitor signal (not yet used: there are changes / bug-fixes in 'da00' planned for the near future) --- eos/event_data_types.py | 7 ++++++ eos/event_handling.py | 4 ++- eos/file_reader.py | 56 ++++++++++++++++++++++++++++------------- eos/options.py | 4 +-- eos/projection.py | 2 +- 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/eos/event_data_types.py b/eos/event_data_types.py index 2ccfbf6..466d463 100644 --- a/eos/event_data_types.py +++ b/eos/event_data_types.py @@ -29,11 +29,17 @@ class AmorTiming: chopperPhase: float tau: float +@dataclass +class DA00EventBuffer: + signal: np.ndarray + time: np.ndarray + # Structured datatypes used for event streams EVENT_TYPE = np.dtype([('tof', np.float64), ('pixelID', np.uint32), ('mask', np.int32)]) PACKET_TYPE = np.dtype([('start_index', np.uint32), ('time', np.int64)]) PULSE_TYPE = np.dtype([('time', np.int64), ('monitor', np.float32)]) PC_TYPE = np.dtype([('current', np.float32), ('time', np.int64)]) +BM_TYPE = np.dtype([('value', np.int64), ('time', np.int64)]) LOG_TYPE = np.dtype([('value', np.float32), ('time', np.int64)]) # define the bitmask for individual event filters @@ -63,6 +69,7 @@ class AmorEventStream: packets: np.recarray # PACKET_TYPE pulses: np.recarray # PULSE_TYPE proton_current: np.recarray # PC_TYPE + beam_monitor: np.recarray # BM_TYPE device_logs: Dict[str, np.recarray] = field(default_factory=dict) # LOG_TYPE class EventDatasetProtocol(Protocol): diff --git a/eos/event_handling.py b/eos/event_handling.py index 84c70c3..d062239 100644 --- a/eos/event_handling.py +++ b/eos/event_handling.py @@ -91,6 +91,8 @@ class AssociatePulseWithMonitor(EventDataAction): * 2*dataset.timing.tau * 1e-3 elif self.monitorType==MonitorType.time: dataset.data.pulses.monitor = 2*dataset.timing.tau + elif self.monitorType==MonitorType.neutron_monitor: + dataset.data.pulses.monitor = dataset.data.beam_monitor.value / 1e4 else: # pulses dataset.data.pulses.monitor = 1 @@ -201,4 +203,4 @@ class ApplyMask(EventDataAction): d.device_logs[key] = np.recarray(d.pulses.shape, dtype = log.dtype) d.device_logs[key].time = d.pulses.time d.device_logs[key].value = d.pulses[key] - dataset.update_info_from_logs() \ No newline at end of file + dataset.update_info_from_logs() diff --git a/eos/file_reader.py b/eos/file_reader.py index 7a209bd..e8506b2 100644 --- a/eos/file_reader.py +++ b/eos/file_reader.py @@ -7,7 +7,6 @@ import sys import h5py import numpy as np import logging -import subprocess from datetime import datetime @@ -17,7 +16,7 @@ from orsopy.fileio.model_language import SampleModel from . import const, compat from .header import Header from .event_data_types import AmorGeometry, AmorTiming, AmorEventStream, LOG_TYPE, PACKET_TYPE, EVENT_TYPE, PULSE_TYPE, \ - PC_TYPE + PC_TYPE, BM_TYPE, DA00EventBuffer try: import zoneinfo @@ -33,7 +32,7 @@ NO_DEFAULT_VALUE = object() # just for allowing None default value in AmorHeader class AmorHeader: """ - Collects header information from Amor NeXus fiel without reading event data. + Collects header information from Amor NeXus file without reading event data. """ # mapping of names to (hdf_path, dtype, nicos_key[, suffix]) hdf_paths = dict( @@ -44,7 +43,7 @@ class AmorHeader: user_affiliation=('entry1/user/affiliation', str), sample_name=('entry1/sample/name', str), sample_model=('entry1/sample/model', str), - sample_geometry=('entry1/sample/geometry', dict), + sample_geometry=('entry1/sample/geometry', str), source_name=('entry1/Amor/source/name', str), start_time=('entry1/start_time', str), start_time_fallback=('entry1/Amor/measurement_configuration/start_time', str), @@ -52,7 +51,7 @@ class AmorHeader: chopper_separation=('entry1/Amor/chopper/pair_separation', float), chopper_distance=('entry1/Amor/chopper/distance', float), - detector_distance=('entry1/Amor/detector/transformation/distance', float), + detector_distance=('entry1/Amor/detector_stage/transformation/distance', float), sample_temperature=('entry1/sample/environment/temperature', float), sample_magnetic_field=('entry1/sample/environment/magnetic_field', float), sample_current=('entry1/sample/environment/current', float), @@ -68,14 +67,14 @@ class AmorHeader: virtual_source_vertical=('entry1/Amor/virtual_source/vertical', float), ch1_trigger_phase=('entry1/Amor/chopper/ch1_trigger_phase', float, 'ch1_trigger_phase'), ch2_trigger_phase=('entry1/Amor/chopper/ch2_trigger_phase', float, 'ch2_trigger_phase'), - chopper_speed=('entry1/Amor/chopper/rotation_speed', float, 'chopper_phase'), + chopper_speed=('entry1/Amor/chopper/rotation_speed', float, 'chopper_speed'), chopper_phase=('entry1/Amor/chopper/phase', float, 'chopper_phase'), polarization_config_label=('entry1/Amor/polarization/configuration', int, 'polarization_config_label', '/*'), data=('entry1/detector/data', None), # data group used to load events from trigger=('entry1/detector/trigger', float), - monitor=('entry1/detector/monitor', float), + beam_monitor=('entry1/beam_monitor', None), # special data group (da00 flat-buffer scheme) proton_current=('entry1/detector/proton_current', float), acquisition_filter=('entry1/detector/acquisition_filter', int), ) @@ -147,6 +146,9 @@ class AmorHeader: return output, unit else: return output + elif hdfgrp.attrs.get('writer_module', None) == 'da00': + # da00 float-buffer stream data + return DA00EventBuffer(signal=hdfgrp['signal'][:], time=hdfgrp['time'][:]) elif dtype is str: return self.read_string(hdf_path) else: @@ -262,7 +264,8 @@ class AmorHeader: ) sample_geometry = self.rv('sample_geometry', default=None) if sample_geometry: - self.sample.geometry = sample_geometry + import yaml + self.sample.geometry = yaml.safe_load(sample_geometry) # while event times are not evaluated, use first value reported in file for SEE for param in self.SEE_PARAMS: key = f'sample_{param}' @@ -454,14 +457,16 @@ class AmorEventData(AmorHeader): events.pixelID = data_group['event_id'][self.first_index:self.last_index+1] events.mask = 0 + self._beam_monitor = self.rv('beam_monitor', default=None) pulses = self.read_chopper_trigger_stream(packets) current = self.read_proton_current_stream(packets) + beam_monitor = self.read_proton_current_stream(packets) + #beam_monitor = self.read_beam_monitor_stream(packets) TODO # read parameter logs not present in old files to ensure they are in self._log_keys if they exist - _monitor = self.rv('monitor', default=None) _acquisition_filter = self.rv('acquisition_filter', default=None) - self.data = AmorEventStream(events, packets, pulses, current) + self.data = AmorEventStream(events, packets, pulses, current, beam_monitor) if self.first_index>0 and not self.EOF: # label the file name if not all events were used @@ -488,8 +493,11 @@ class AmorEventData(AmorHeader): sample_keys = [f'sample_{param}' for param in self.SEE_PARAMS] relevant_items += sample_keys for key, log in self.data.device_logs.items(): + # if SE is missing there can be a missing key or empty dataset if key not in relevant_items: continue + if log.value.size == 0: + continue if log.value.dtype in [np.int8, np.int16, np.int32, np.int64]: # for integer items (flags) report the most common one value = np.bincount(log.value).argmax() @@ -515,10 +523,8 @@ class AmorEventData(AmorHeader): pulseTimeS = chopper1TriggerTime else: logging.critical(' No chopper trigger data available, using event steram instead, pulse filtering will fail!') - data_group = self.hdf[self.hdf_paths['data'][0]] - startTime = np.array(data_group['event_time_zero'][0], dtype=np.int64) - stopTime = np.array(data_group['event_time_zero'][-2], dtype=np.int64) - pulseTimeS = np.arange(startTime, stopTime, self.timing.tau*1e9, dtype=np.int64) + startTime = np.array(self._beam_monitor.time[0], dtype=np.int64) + pulseTimeS = self._beam_monitor.time pulses = np.recarray(pulseTimeS.shape, dtype=PULSE_TYPE) pulses.time = pulseTimeS pulses.monitor = 1. # default is monitor pulses as it requires no calculation @@ -542,6 +548,20 @@ class AmorEventData(AmorHeader): (proton_current.time<=packets.time[-1])] return proton_current + def read_beam_monitor_stream(self, packets): + _signal = self._beam_monitor.signal + _time = self._beam_monitor.time + plse = np.array([0, 5, 6, 11]) + base = np.array([2, 3, 8, 9]) + i = 0 + beam_monitor = np.recarray(shape=np.shape(packets.time)[0], dtype=BM_TYPE) + for tt, tme in enumerate(_time): + if (tme>=packets.time[0])&(tme<=packets.time[-1]): + beam_monitor.value[i] = _signal[tt][plse].sum() - _signal[tt][base].sum() + beam_monitor.time[i] = tme + i += 1 + return beam_monitor[:i+1] # TODO: the '+1' is strange. Why needed? + def info(self): output = "" for key in ['owner', 'experiment', 'sample', 'instrument_settings']: @@ -558,10 +578,11 @@ class AmorEventData(AmorHeader): new_events = np.concatenate([self.data.events, other.data.events]).view(np.recarray) new_pulses = np.concatenate([self.data.pulses, other.data.pulses]).view(np.recarray) new_proton_current = np.concatenate([self.data.proton_current, other.data.proton_current]).view(np.recarray) + new_beam_monitor = np.concatenate([self.data.beam_monitor, other.data.beam_monitor]).view(np.recarray) new_packets = np.concatenate([self.data.packets, other.data.packets]).view(np.recarray) new_packets.start_index[self.data.packets.shape[0]:] += self.data.events.shape[0] - self.data = AmorEventStream(new_events, new_packets, new_pulses, new_proton_current) - # Indicate that this is amodified dataset, basically counts number of appends as negative indices + self.data = AmorEventStream(new_events, new_packets, new_pulses, new_proton_current, new_beam_monitor) + # Indicate that this is a modified dataset, basically counts number of appends as negative indices self.last_index = min(self.last_index-1, -1) self.file_list += other.file_list @@ -589,5 +610,6 @@ class AmorEventData(AmorHeader): setattr(output, key, value) # TODO: this is not strictly correct, as the packet/event relationship is lost output.data = AmorEventStream(self.data.events[event_filter], self.data.packets, - self.data.pulses[pulse_filter], self.data.proton_current) + self.data.pulses[pulse_filter], self.data.proton_current, + self.data.beam_monitor) return output diff --git a/eos/options.py b/eos/options.py index 4eabd24..cd11738 100644 --- a/eos/options.py +++ b/eos/options.py @@ -247,7 +247,7 @@ class MonitorType(StrEnum): debug = 'x' MONITOR_UNITS = { - MonitorType.neutron_monitor: 'cnts', + MonitorType.neutron_monitor: 'au', MonitorType.proton_charge: 'mC', MonitorType.time: 's', MonitorType.auto: 'various', @@ -273,7 +273,7 @@ class ExperimentConfig(ArgParsable): }, ) chopperSpeed: float = field( - default=500, + default=450, metadata={ 'short': 'cs', 'group': 'instrument settings', diff --git a/eos/projection.py b/eos/projection.py index 2cdc50e..f034ae6 100644 --- a/eos/projection.py +++ b/eos/projection.py @@ -829,4 +829,4 @@ class CombinedProjection(ProjectionInterface): pi.update_plot() # def update_range(self, event): - # ... \ No newline at end of file + # ...