From 8f2695f8a7c648bec2fc4ced1b0dab1bbfcb4569 Mon Sep 17 00:00:00 2001 From: Artur Glavic Date: Mon, 6 Oct 2025 09:54:17 +0200 Subject: [PATCH] Reorganize event actions to separate numba functions --- libeos/event_analysis.py | 55 ++++++++++++++------------------------- libeos/event_handling.py | 56 ++++++++++++++++++++++++---------------- libeos/options.py | 2 +- libeos/reduction.py | 16 ++++++------ 4 files changed, 63 insertions(+), 66 deletions(-) diff --git a/libeos/event_analysis.py b/libeos/event_analysis.py index 48b8df0..5ab55bc 100644 --- a/libeos/event_analysis.py +++ b/libeos/event_analysis.py @@ -1,19 +1,37 @@ """ Define an event dataformat that performs reduction actions like wavelength calculation on per-event basis. +With large number of events these actions can be time consuming so they use numba based functions. """ import numpy as np import logging from typing import Tuple -from numpy.lib.recfunctions import rec_append_fields from . import const from .event_data_types import EventDataAction, EventDatasetProtocol, append_fields, EVENT_BITMASKS -from .helpers import filter_project_x +from .helpers import filter_project_x, merge_frames, extract_walltime from .instrument import Detector from .options import IncidentAngle from .header import Header +class ExtractWalltime(EventDataAction): + def perform_action(self, dataset: EventDatasetProtocol) ->None: + # TODO: fix numba type definition after refactor + wallTime = extract_walltime(dataset.data.events.tof, + dataset.data.packets.start_index.astype(np.uint64), + dataset.data.packets.Time) + logging.debug(f' expending event stream by wallTime') + new_events = append_fields(dataset.data.events, [('wallTime', wallTime.dtype)]) + new_events.wallTime = wallTime + dataset.data.events = new_events + +class MergeFrames(EventDataAction): + def perform_action(self, dataset: EventDatasetProtocol)->None: + 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) + class AnalyzePixelIDs(EventDataAction): def __init__(self, yRange: Tuple[int, int]): @@ -34,17 +52,6 @@ class AnalyzePixelIDs(EventDataAction): ana_events.mask += np.logical_not(mask)*EVENT_BITMASKS['yRange'] d.events = ana_events -class TofTimeCorrection(EventDataAction): - def __init__(self, correct_chopper_opening: bool = True): - self.correct_chopper_opening = correct_chopper_opening - - def perform_action(self, dataset: EventDatasetProtocol) ->None: - d = dataset.data - if self.correct_chopper_opening: - d.events.tof -= ( d.events.delta / 180. ) * dataset.timing.tau - else: - d.events.tof -= ( dataset.geometry.kad / 180. ) * dataset.timing.tau - class CalculateWavelength(EventDataAction): def __init__(self, lambdaRange: Tuple[float, float]): self.lambdaRange = lambdaRange @@ -113,25 +120,3 @@ class FilterQzRange(EventDataAction): if self.qzRange[1]<0.5: d.events.mask += EVENT_BITMASKS["qRange"]*((self.qzRange[0]>d.events.qz) | (d.events.qz>self.qzRange[1])) - -class ApplyMask(EventDataAction): - def __init__(self, bitmask_filter=None): - self.bitmask_filter = bitmask_filter - - def perform_action(self, dataset: EventDatasetProtocol) ->None: - d = dataset.data - logging.info(f' number of events: total = {d.events.shape[0]:7d}, ' - f'filtered = {(d.events.mask!=0).sum():7d}') - if logging.getLogger().level == logging.DEBUG: - # only run this calculation if debug level is actually active - filtered_by_mask = {} - for key, value in EVENT_BITMASKS.items(): - filtered_by_mask[key] = ((d.events.mask & value)!=0).sum() - logging.debug(f" Removed by filters: {filtered_by_mask}") - if self.bitmask_filter is None: - d.events = d.events[d.events.mask==0] - else: - # remove the provided bitmask_filter bits from the events - # this means that all bits that are set in bitmask_filter will NOT be used to filter events - fltr = (d.events.mask & (~self.bitmask_filter)) == 0 - d.events = d.events[fltr] diff --git a/libeos/event_handling.py b/libeos/event_handling.py index 0978fb7..267b61f 100644 --- a/libeos/event_handling.py +++ b/libeos/event_handling.py @@ -1,16 +1,14 @@ """ Calculations performed on AmorEventData. +This module contains actions that do not need the numba base helper functions. Other actions are in event_analysis """ import logging import os import numpy as np -# from numpy.lib.recfunctions import rec_append_fields, append_fields -from . import const from .header import Header from .options import ExperimentConfig, MonitorType -from .event_data_types import EventDatasetProtocol, EventDataAction, append_fields, EVENT_BITMASKS -from .helpers import merge_frames, extract_walltime +from .event_data_types import EventDatasetProtocol, EventDataAction, EVENT_BITMASKS class ApplyPhaseOffset(EventDataAction): def __init__(self, chopperPhaseOffset: float): @@ -62,17 +60,6 @@ class CorrectChopperPhase(EventDataAction): 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 - wallTime = extract_walltime(dataset.data.events.tof, - dataset.data.packets.start_index.astype(np.uint64), - dataset.data.packets.Time) - logging.debug(f' expending event stream by wallTime') - new_events = append_fields(dataset.data.events, [('wallTime', wallTime.dtype)]) - new_events.wallTime = wallTime - dataset.data.events = new_events - class CorrectSeriesTime(EventDataAction): def __init__(self, seriesStartTime): self.seriesStartTime = np.int64(seriesStartTime) @@ -146,7 +133,6 @@ class AssociatePulseWithMonitor(EventDataAction): return pulseCurrentS - class FilterStrangeTimes(EventDataAction): def perform_action(self, dataset: EventDatasetProtocol)->None: filter_e = np.logical_not(dataset.data.events.tof<=2*dataset.timing.tau) @@ -155,9 +141,35 @@ class FilterStrangeTimes(EventDataAction): logging.warning(f' strange times: {filter_e.sum()}') -class MergeFrames(EventDataAction): - def perform_action(self, dataset: EventDatasetProtocol)->None: - 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) +class TofTimeCorrection(EventDataAction): + def __init__(self, correct_chopper_opening: bool = True): + self.correct_chopper_opening = correct_chopper_opening + + def perform_action(self, dataset: EventDatasetProtocol) ->None: + d = dataset.data + if self.correct_chopper_opening: + d.events.tof -= ( d.events.delta / 180. ) * dataset.timing.tau + else: + d.events.tof -= ( dataset.geometry.kad / 180. ) * dataset.timing.tau + +class ApplyMask(EventDataAction): + def __init__(self, bitmask_filter=None): + self.bitmask_filter = bitmask_filter + + def perform_action(self, dataset: EventDatasetProtocol) ->None: + d = dataset.data + logging.info(f' number of events: total = {d.events.shape[0]:7d}, ' + f'filtered = {(d.events.mask!=0).sum():7d}') + if logging.getLogger().level == logging.DEBUG: + # only run this calculation if debug level is actually active + filtered_by_mask = {} + for key, value in EVENT_BITMASKS.items(): + filtered_by_mask[key] = ((d.events.mask & value)!=0).sum() + logging.debug(f" Removed by filters: {filtered_by_mask}") + if self.bitmask_filter is None: + d.events = d.events[d.events.mask==0] + else: + # remove the provided bitmask_filter bits from the events + # this means that all bits that are set in bitmask_filter will NOT be used to filter events + fltr = (d.events.mask & (~self.bitmask_filter)) == 0 + d.events = d.events[fltr] diff --git a/libeos/options.py b/libeos/options.py index 3269a00..d9056af 100644 --- a/libeos/options.py +++ b/libeos/options.py @@ -240,7 +240,7 @@ class ExperimentConfig(ArgParsable): }, ) - incidentAngle: str = field( + incidentAngle: IncidentAngle = field( default=IncidentAngle.alphaF, metadata={ 'short': 'ai', diff --git a/libeos/reduction.py b/libeos/reduction.py index bb330e7..4a5dcc6 100644 --- a/libeos/reduction.py +++ b/libeos/reduction.py @@ -48,32 +48,32 @@ class AmorReduction: self.normevent_actions = eh.ApplyPhaseOffset(self.experiment_config.chopperPhaseOffset) self.normevent_actions |= eh.CorrectChopperPhase() if self.experiment_config.monitorType in [MonitorType.proton_charge, MonitorType.debug]: - self.normevent_actions |= eh.ExtractWalltime() + self.normevent_actions |= ea.ExtractWalltime() self.normevent_actions |= eh.AssociatePulseWithMonitor(self.experiment_config.monitorType, self.experiment_config.lowCurrentThreshold) self.normevent_actions |= eh.FilterStrangeTimes() - self.normevent_actions |= eh.MergeFrames() + self.normevent_actions |= ea.MergeFrames() self.normevent_actions |= ea.AnalyzePixelIDs(self.experiment_config.yRange) - self.normevent_actions |= ea.TofTimeCorrection(self.experiment_config.incidentAngle==IncidentAngle.alphaF) + self.normevent_actions |= eh.TofTimeCorrection(self.experiment_config.incidentAngle==IncidentAngle.alphaF) self.normevent_actions |= ea.CalculateWavelength(self.experiment_config.lambdaRange) - self.normevent_actions |= ea.ApplyMask() + self.normevent_actions |= eh.ApplyMask() # Actions on datasets not used for normalization self.dataevent_actions = eh.ApplyPhaseOffset(self.experiment_config.chopperPhaseOffset) self.dataevent_actions |= eh.ApplyParameterOverwrites(self.experiment_config) # some actions use instrument parameters, change before that self.dataevent_actions |= eh.CorrectChopperPhase() - self.dataevent_actions |= eh.ExtractWalltime() + self.dataevent_actions |= ea.ExtractWalltime() self.dataevent_time_correction = eh.CorrectSeriesTime(0) # will be set from first dataset self.dataevent_actions |= self.dataevent_time_correction self.dataevent_actions |= eh.AssociatePulseWithMonitor(self.experiment_config.monitorType, self.experiment_config.lowCurrentThreshold) self.dataevent_actions |= eh.FilterStrangeTimes() - self.dataevent_actions |= eh.MergeFrames() + self.dataevent_actions |= ea.MergeFrames() self.dataevent_actions |= ea.AnalyzePixelIDs(self.experiment_config.yRange) - self.dataevent_actions |= ea.TofTimeCorrection(self.experiment_config.incidentAngle==IncidentAngle.alphaF) + self.dataevent_actions |= eh.TofTimeCorrection(self.experiment_config.incidentAngle==IncidentAngle.alphaF) self.dataevent_actions |= ea.CalculateWavelength(self.experiment_config.lambdaRange) self.dataevent_actions |= ea.CalculateQ(self.experiment_config.incidentAngle) self.dataevent_actions |= ea.FilterQzRange(self.reduction_config.qzRange) - self.dataevent_actions |= ea.ApplyMask() + self.dataevent_actions |= eh.ApplyMask() self.grid = LZGrid(self.reduction_config.qResolution, self.reduction_config.qzRange)