From 5ecdecfe24febc9ba2110f0910d12e11d0162c8c Mon Sep 17 00:00:00 2001 From: Artur Glavic Date: Thu, 2 Oct 2025 12:23:23 +0200 Subject: [PATCH] Extract some actions from file reader to event actions as they depend on series time or parameter overwrites --- libeos/event_handling.py | 66 ++++++++++++++++++++++++++++++++++--- libeos/file_reader.py | 44 ++++++++++++------------- tests/test_full_analysis.py | 4 +-- 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/libeos/event_handling.py b/libeos/event_handling.py index 17ed3fe..b946098 100644 --- a/libeos/event_handling.py +++ b/libeos/event_handling.py @@ -2,12 +2,65 @@ Calculations performed on AmorEventData. """ import logging +import os import numpy as np from . import const -from .options import MonitorType +from .header import Header +from .options import ExperimentConfig, MonitorType from .event_data_types import EventDatasetProtocol, EventDataAction -from .helpers import merge_frames +from .helpers import merge_frames, extract_walltime + +class ApplyParameterOverwrites(EventDataAction): + def __init__(self, config: ExperimentConfig): + self.config=config + + def perform_action(self, dataset: EventDatasetProtocol) ->None: + if self.config.muOffset: + logging.debug(f' set muOffset = {self.config.muOffset}') + dataset.geometry.mu += self.config.muOffset + if self.config.mu: + logging.debug(f' replaced mu = {dataset.geometry.mu} with {self.config.mu}') + dataset.geometry.mu = self.config.mu + if self.config.nu: + logging.debug(f' replaced nu = {dataset.geometry.nu} with {self.config.nu}') + dataset.geometry.nu = self.config.nu + if self.config.chopperPhaseOffset: + logging.debug( + f' replaced ch1TriggerPhase = {dataset.timing.ch1TriggerPhase} ' + f'with {self.config.chopperPhaseOffset}') + dataset.timing.ch1TriggerPhase = self.config.chopperPhaseOffset + logging.info(f' mu = {dataset.geometry.mu:6.3f}, ' + f'nu = {dataset.geometry.nu:6.3f}, ' + f'kap = {dataset.geometry.kap:6.3f}, ' + f'kad = {dataset.geometry.kad:6.3f}') + + def update_header(self, header:Header) ->None: + if self.config.sampleModel: + if 'yml' in self.config.sampleModel or 'yaml' in self.config.sampleModel: + import yaml + if os.path.isfile(self.config.sampleModel): + with open(self.config.sampleModel, 'r') as model_yml: + model = yaml.safe_load(model_yml) + else: + logging.warning(f' ! the file {self.config.sampleModel}.yml does not exist. Ignored!') + else: + model = dict(stack=self.config.sampleModel) + logging.debug(f' set sample.model = {self.config.sampleModel}') + header.sample.model = model + + +class CorrectChopperPhase(EventDataAction): + def perform_action(self, dataset: EventDatasetProtocol) ->None: + dataset.data.events.tof += dataset.timing.tau*(dataset.timing.ch1TriggerPhase-dataset.timing.chopperPhase/2)/180 + + +class ExtractWalltime(EventDataAction): + def perform_action(self, dataset: EventDatasetProtocol) ->None: + # TODO: fix numba type definition after refactor + dataset.data.events.wallTime = extract_walltime(dataset.data.events.tof, + dataset.data.packets.start_index.astype(np.uint64), + dataset.data.packets.Time) class CorrectSeriesTime(EventDataAction): @@ -18,10 +71,11 @@ class CorrectSeriesTime(EventDataAction): dataset.data.pulses.time -= self.seriesStartTime dataset.data.events.wallTime -= self.seriesStartTime dataset.data.proton_current.time -= self.seriesStartTime - start, stop = dataset.data.proton_current.time[0], dataset.data.proton_current.time[-1] - logging.debug(f' wall time from {start:6.1f} s to {stop/1e9:6.1f} s, ' + start, stop = dataset.data.events.wallTime[0], dataset.data.events.wallTime[-1] + logging.debug(f' wall time from {start/1e9:6.1f} s to {stop/1e9:6.1f} s, ' f'series time = {self.seriesStartTime/1e9:6.1f}') + class AssociatePulseWithMonitor(EventDataAction): def __init__(self, monitorType:MonitorType, lowCurrentThreshold:float): self.monitorType = monitorType @@ -78,6 +132,7 @@ class AssociatePulseWithMonitor(EventDataAction): return pulseCurrentS + class FilterStrangeTimes(EventDataAction): def perform_action(self, dataset: EventDatasetProtocol)->None: filter_e = (dataset.data.events.tof<=2*dataset.timing.tau) @@ -85,9 +140,10 @@ class FilterStrangeTimes(EventDataAction): if not filter_e.all(): logging.warning(f' strange times: {np.logical_not(filter_e).sum()}') + class MergeFrames(EventDataAction): def perform_action(self, dataset: EventDatasetProtocol)->None: - tofCut = const.lamdaCut+dataset.geometry.chopperDetectorDistance/const.hdm*1e-13 + tofCut = const.lamdaCut*dataset.geometry.chopperDetectorDistance/const.hdm*1e-13 total_offset = (tofCut + dataset.timing.tau * (dataset.timing.ch1TriggerPhase + dataset.timing.chopperPhase/2)/180) dataset.data.events.tof = merge_frames(dataset.data.events.tof, tofCut, dataset.timing.tau, total_offset) diff --git a/libeos/file_reader.py b/libeos/file_reader.py index 9643663..8c7b267 100644 --- a/libeos/file_reader.py +++ b/libeos/file_reader.py @@ -18,9 +18,8 @@ from orsopy.fileio.model_language import SampleModel from . import const, event_handling as eh, event_analysis as ea from .header import Header -from .helpers import extract_walltime from .event_data_types import AmorGeometry, AmorTiming, AmorEventStream, PACKET_TYPE, EVENT_TYPE, PULSE_TYPE, PC_TYPE -from .options import ExperimentConfig, IncidentAngle, ReaderConfig +from .options import ExperimentConfig, IncidentAngle, ReaderConfig, MonitorType try: import zoneinfo @@ -101,7 +100,7 @@ class AmorEventData: eventStartTime: np.int64 - def __init__(self, fileName:Union[str, h5py.File, BinaryIO], first_index:int=0, max_events:int=1_000_000): + def __init__(self, fileName:Union[str, h5py.File, BinaryIO], first_index:int=0, max_events:int=100_000_000): if type(fileName) is str: self.fileName = fileName self.hdf = h5py.File(fileName, 'r', swmr=True) @@ -119,9 +118,7 @@ class AmorEventData: self.read_event_stream() # actions applied to any dataset - self.correct_for_chopper_phases() self.read_chopper_trigger_stream() - self.extract_walltime() self.read_proton_current_stream() if type(fileName) is str: @@ -220,7 +217,7 @@ class AmorEventData: chopperSeparation, detectorDistance, chopperDetectorDistance) self.timing = AmorTiming(ch1TriggerPhase, ch2TriggerPhase, chopperSpeed, chopperPhase, tau) - polarizationConfigLabel = self._replace_if_missing('polarization/configuration/value', 'polarizer_config_label', int) + polarizationConfigLabel = self._replace_if_missing('polarization/configuration/average_value', 'polarizer_config_label', int) polarizationConfig = fileio.Polarization(polarizationConfigs[polarizationConfigLabel]) logging.debug(f' polarization configuration: {polarizationConfig} (index {polarizationConfigLabel})') @@ -260,6 +257,7 @@ class AmorEventData: """ Add dataset information into an existing header. """ + logging.info(f' meta data from: {self.fileName}') header.owner = self.owner header.experiment = self.experiment header.sample = self.sample @@ -298,12 +296,8 @@ class AmorEventData: events.pixelID = self.hdf['/entry1/Amor/detector/data/event_id'][self.first_index:self.last_index+1] self.data = AmorEventStream(events, packets) - def correct_for_chopper_phases(self): - self.data.events.tof += self.timing.tau*(self.timing.ch1TriggerPhase-self.timing.chopperPhase/2)/180 - def read_chopper_trigger_stream(self): - chopper1TriggerTime = np.array(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_zero'][:-2], - dtype=np.int64) + chopper1TriggerTime = np.array(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_zero'][:-2], dtype=np.int64) #self.chopper2TriggerTime = self.chopper1TriggerTime + np.array(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time'][:-2], dtype=np.int64) # + np.array(self.hdf['entry1/Amor/chopper/ch2_trigger/event_time_offset'][:], dtype=np.int64) if np.shape(chopper1TriggerTime)[0] > 2: @@ -322,12 +316,6 @@ class AmorEventData: self.data.pulses = pulses self.eventStartTime = startTime - def extract_walltime(self): - # TODO: fix numba type definition after refactor - self.data.events.wallTime = extract_walltime(self.data.events.tof, - self.data.packets.start_index.astype(np.uint64), - self.data.packets.Time) - def read_proton_current_stream(self): proton_current = np.recarray(self.hdf['entry1/Amor/detector/proton_current/time'].shape, dtype=PC_TYPE) proton_current.time = self.hdf['entry1/Amor/detector/proton_current/time'][:] @@ -410,7 +398,12 @@ class AmorData: def prepare_actions(self): # setup all actions performed in origin AmorData, time correction requires first dataset start time - self.event_actions = eh.AssociatePulseWithMonitor(self.config.monitorType, self.config.lowCurrentThreshold) + self.time_correction = eh.CorrectSeriesTime(0) + self.event_actions = eh.ApplyParameterOverwrites(self.config) # some actions use instrument parameters, change before that + self.event_actions |= eh.CorrectChopperPhase() + self.event_actions |= eh.ExtractWalltime() + self.event_actions |= self.time_correction + self.event_actions |= eh.AssociatePulseWithMonitor(self.config.monitorType, self.config.lowCurrentThreshold) self.event_actions |= eh.FilterStrangeTimes() self.event_actions |= eh.MergeFrames() self.event_actions |= ea.AnalyzePixelIDs(self.config.yRange) @@ -421,16 +414,23 @@ class AmorData: def process(self): self.dataset = AmorEventData(self.file_list[0]) - time_correction = eh.CorrectSeriesTime(self.dataset.eventStartTime) - time_correction(self.dataset) + if self.config.monitorType==MonitorType.auto: + if self.dataset.data.proton_current.current.sum()>1: + self.monitorType = MonitorType.proton_charge + logging.warning(' monitor type set to "proton current"') + else: + self.monitorType = MonitorType.time + logging.warning(' monitor type set to "time"') + # update actions to sue selected monitor + self.prepare_actions() + + self.time_correction.seriesStartTime = self.dataset.eventStartTime self.event_actions(self.dataset) if not self.norm: self.dataset.update_header(self.header) - time_correction.update_header(self.header) self.event_actions.update_header(self.header) for fi in self.file_list[1:]: di = AmorEventData(fi) - time_correction(di) self.event_actions(di) self.dataset.append(di) diff --git a/tests/test_full_analysis.py b/tests/test_full_analysis.py index c8f3dde..1235a02 100644 --- a/tests/test_full_analysis.py +++ b/tests/test_full_analysis.py @@ -50,7 +50,7 @@ class FullAmorTest(TestCase): chopperPhaseOffset=-5, monitorType=self._field_defaults['ExperimentConfig']['monitorType'], lowCurrentThreshold=self._field_defaults['ExperimentConfig']['lowCurrentThreshold'], - yRange=(11., 41.), + yRange=(11, 41), lambdaRange=(2., 15.), incidentAngle=self._field_defaults['ExperimentConfig']['incidentAngle'], mu=0, @@ -90,7 +90,7 @@ class FullAmorTest(TestCase): chopperPhaseOffset=-5, monitorType=self._field_defaults['ExperimentConfig']['monitorType'], lowCurrentThreshold=self._field_defaults['ExperimentConfig']['lowCurrentThreshold'], - yRange=(11., 41.), + yRange=(11, 41), lambdaRange=(2., 15.), incidentAngle=self._field_defaults['ExperimentConfig']['incidentAngle'], mu=0,