Reorganize event actions to separate numba functions

This commit is contained in:
2025-10-06 09:54:17 +02:00
parent f6bda1160e
commit 8f2695f8a7
4 changed files with 63 additions and 66 deletions

View File

@@ -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]

View File

@@ -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]

View File

@@ -240,7 +240,7 @@ class ExperimentConfig(ArgParsable):
},
)
incidentAngle: str = field(
incidentAngle: IncidentAngle = field(
default=IncidentAngle.alphaF,
metadata={
'short': 'ai',

View File

@@ -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)