Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Valdrada, the trigger simulation city #795

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
33 changes: 30 additions & 3 deletions invisible_cities/cities/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def create_timestamp(rate: float) -> float:

Returns
-------
Function to calculate timestamp for the given rate with
Function to calculate timestamp for the given rate with
event_number as parameter.
"""

Expand All @@ -142,12 +142,12 @@ def create_timestamp(rate: float) -> float:
def create_timestamp_(event_number: Union[int, float]) -> float:
"""
Calculates timestamp for a given Event Number and Rate.

Parameters
----------
event_number : Union[int, float]
ID value of the current event.

Returns
-------
Calculated timestamp : float
Expand Down Expand Up @@ -326,6 +326,33 @@ def deconv_pmt(RWF):
return deconv_pmt


def deconv_pmt_fpga(dbfile : str
,run_number : int
,selection : List[int] = None
) -> Callable:
'''
Apply deconvolution as done in the FPGA.

Parameters
----------
dbfile : Database to be used
run_number : Run number of the database
selection : List of PMT IDs to apply deconvolution to.

Returns
----------
deconv_pmt : Function that will apply the deconvolution.
'''
DataPMT = load_db.DataPMT(dbfile, run_number = run_number)
pmt_active = np.nonzero(DataPMT.Active.values)[0].tolist() if selection is None else selection
coeff_c = DataPMT.coeff_c .values.astype(np.double)[pmt_active]
coeff_blr = DataPMT.coeff_blr.values.astype(np.double)[pmt_active]
def deconv_pmt(RWF):
return zip(*map(blr.deconvolve_signal_fpga, RWF[pmt_active],
coeff_c , coeff_blr ))
return deconv_pmt


def get_run_number(h5in):
if "runInfo" in h5in.root.Run: return h5in.root.Run.runInfo[0]['run_number']
elif "RunInfo" in h5in.root.Run: return h5in.root.Run.RunInfo[0]['run_number']
Expand Down
172 changes: 172 additions & 0 deletions invisible_cities/cities/valdrada.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"""
-----------------------------------------------------------------------
valdrada
-----------------------------------------------------------------------

This city finds simulates the trigger procedure at the FPGA over PMT waveforms.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This city finds simulates the trigger procedure at the FPGA over PMT waveforms.
This city simulates the trigger procedure at the FPGA over PMT waveforms.

This includes a number of tasks:
- Use a moving average to obtain the baseline
- Truncate to integers the deconvolution output
- Find trigger candidates and compute their characteristics, taking into
account the unique circumstances derived from working on a continuous scheme
(for example, delay in signals)
- Evaluate coincidences between trigger candidates.

A number of general configuration parameters are needed (input as a dict, trigger_config):
- coincidence_window : Time window (in time bins) in which valid trigger coincidences are counted
- discard_width : Any trigger with width less than this parameter is discarded.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

units?

- multipeak : Dictionary with multipeak protection parameters, otherwise None.
- q_min : Integrated charge threshold of a post-trigger peak to discard
a trigger due to multipeak protection. In ADC counts.
- time_min : Minimum width of a post-trigger peak to discard a trigger due to
multipeak protection. In time bins.
- time_after : For how long is the multipeak protection evaluated after a
valid trigger. In mus.

A individual trigger configuration can be given per channel, through a dict
with keys equal to PMT IDs, which marks the validity range of the peak
characteristics:
- q_min, q_max : Range for the integrated charge of the peak (q_min < q < q_max).
In ADC counts.
- time_min, time_max : Range for the peak width (time_min <= width < time_max).
In time mus.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In time mus.
In mus.

- baseline_dev, amp_max : Range for peak height (baseline_dev < height < amp_max).
In ADC counts.
- pulse_valid_ext : Time allowed for a pulse to go below baseline_dev. In ns.

The result of the city is a dataframe containing the event ID and PMT ID of each
trigger candidate. For each trigger candidate a number of parameters is computed:
- trigger_time : time bin at which the trigger candidate starts.
- q : integrated ADC counts within the peak.
- width : Length of the peak in time bins.
- height : Maximum ADC values in a given time bien within the peak.
- baseline : Value of the baseline at trigger_time.
- max_height : Maximum (minimum due to PMT negative signals) height in the wvf.

Additionally, a set of flags are assigned depending on wether the parameters are
within range of the trigger configuration:
- valid_q : If peak is within q range.
- valid_w : If peak is within width range.
- valid_h : If peak is within height range.
- valid_peak : Only if 'multipeak' is active, True if there isn't a post-trigger
candidate with the configuration parameters.
- valid_all : boolean and of previous valid flags.

Finally, a series of coincidence-related values are also given:
- n_coinc : Number of valid triggers within the coincidence_window,
starting at the trigger trigger_time and including the trigger itself.
-1 indicates no valid trigger (including the trigger itself).
- closest_ttime : Time difference to closest valid trigger.
-1 if there are none aside from the trigger itself.
- closest_pmt : PMT ID of the closest valid trigger.
-1 if there are none aside from the trigger itself.
"""

import tables as tb
import numpy as np

from .. reco import tbl_functions as tbl
from .. io .run_and_event_io import run_and_event_writer
from .. io .trigger_io import trigger_writer, trigger_dst_writer

from .. dataflow import dataflow as fl
from .. dataflow.dataflow import push
from .. dataflow.dataflow import pipe
from .. dataflow.dataflow import sink

from . components import city
from . components import print_every
from . components import collect
from . components import copy_mc_info
from . components import deconv_pmt_fpga
from . components import WfType
from . components import wf_from_files
from . components import get_number_of_active_pmts

from .. reco.trigger_functions import retrieve_trigger_information
from .. reco.trigger_functions import get_trigger_candidates
from .. reco.trigger_functions import check_trigger_coincidence

from .. core import system_of_units as units


def check_empty_trigger(triggers) -> bool:
"""
Filter for valdrada flow.
The flow stops if there are no candidate triggers/
"""
return len(triggers) > 0

@city
def valdrada(files_in, file_out, compression, event_range, print_mod, detector_db, run_number,
trigger_config = dict(), channel_config = dict()):

# Change time units into number of waveform bins.
if trigger_config['multipeak'] is not None:
trigger_config['multipeak']['time_min' ] /= 25*units.ns
trigger_config['multipeak']['time_after'] /= 25*units.ns
for k in channel_config.keys():
channel_config[k]['time_min'] /= 25*units.ns
channel_config[k]['time_max'] /= 25*units.ns
Comment on lines +105 to +110
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't these be forced to be ints?

channel_config[k]['pulse_valid_ext'] = int(channel_config[k]['pulse_valid_ext']/25*units.ns)

#### Define data transformations
# Raw WaveForm to deconvolved waveforms
rwf_to_cwf = fl.map(deconv_pmt_fpga(detector_db, run_number, list(channel_config.keys())),
args = "pmt",
out = ("cwf", "baseline"))

# Extract all possible trigger candidates of each PMT
trigger_on_channels = fl.map(retrieve_trigger_information(channel_config, trigger_config),
args = ("pmt", "cwf", "baseline", "event_number"),
out = "triggers")

# Add coincidence between channels
check_coincidences = fl.map(check_trigger_coincidence(trigger_config['coincidence_window']),
item = "triggers")

# Filter events with zero triggers
filter_empty_trigger = fl.map(check_empty_trigger,
args = "triggers",
out = "empty_trigger")

event_count_in = fl.spy_count()
event_count_out = fl.spy_count()
events_no_triggers = fl.count_filter(bool, args = "empty_trigger")
evtnum_collect = collect()

with tb.open_file(file_out, "w", filters = tbl.filters(compression)) as h5out:
# Define writers...
write_event_info_ = run_and_event_writer(h5out)
write_trigger_info_ = trigger_writer (h5out, get_number_of_active_pmts(detector_db, run_number))
write_trigger_dst_ = trigger_dst_writer (h5out)
# ... and make them sinks

write_event_info = sink(write_event_info_ , args=( "run_number" , "event_number" , "timestamp" ))
write_trigger_info = sink(write_trigger_info_, args=( "trigger_type", "trigger_channels" ))
write_trigger_dst = sink(write_trigger_dst_ , args= "triggers" )

result = push(source = wf_from_files(files_in, WfType.rwf),
pipe = pipe(fl.slice(*event_range, close_all=True),
print_every(print_mod),
event_count_in.spy,
rwf_to_cwf,
trigger_on_channels,
filter_empty_trigger,
events_no_triggers.filter,
check_coincidences,
event_count_out.spy,
fl.branch("event_number", evtnum_collect.sink),
fl.fork(write_trigger_dst ,
write_event_info ,
write_trigger_info)),
result = dict(events_in = event_count_in .future,
events_out = event_count_out .future,
evtnum_list = evtnum_collect .future,
events_pass = events_no_triggers.future))

if run_number <= 0:
copy_mc_info(files_in, h5out, result.evtnum_list,
detector_db, run_number)

return result
53 changes: 53 additions & 0 deletions invisible_cities/cities/valdrada_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import os
import tables as tb

from . valdrada import valdrada
from .. core.testing_utils import assert_tables_equality


def test_valdrada_contains_all_tables(trigger_config):
conf, PATH_OUT = trigger_config
valdrada(**conf)
with tb.open_file(PATH_OUT) as h5out:
assert "Run" in h5out.root
assert "Run/events" in h5out.root
assert "Run/runInfo" in h5out.root
assert "Trigger" in h5out.root
assert "Trigger/events" in h5out.root
assert "Trigger/trigger" in h5out.root
assert "Trigger/DST" in h5out.root


def test_valdrada_exact_result_multipeak(ICDATADIR, trigger_config):
true_out = os.path.join(ICDATADIR, "exact_result_multipeak_valdrada.h5")
conf, PATH_OUT = trigger_config
valdrada(**conf)

tables = ("Trigger/events", "Trigger/trigger", "Trigger/DST",
"Run/events" , "Run/runInfo")

with tb.open_file(true_out) as true_output_file:
with tb.open_file(PATH_OUT) as output_file:
for table in tables:
assert hasattr(output_file.root, table)
got = getattr( output_file.root, table)
expected = getattr(true_output_file.root, table)
assert_tables_equality(got, expected)


def test_valdrada_exact_result(ICDATADIR, trigger_config):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about

@mark.parametrize("multipeak", (True, False))
def test_valdrada_exact_result(ICDATADIR, trigger_config, multipeak)
    ...
    if not multipeak:
        conf['trigger_config']['multipeak'] = None

to combine both tests? You would need to add true_out to the parametrize as well, but it's worth it.

true_out = os.path.join(ICDATADIR, "exact_result_valdrada.h5")
conf, PATH_OUT = trigger_config
conf['trigger_config']['multipeak'] = None
valdrada(**conf)

tables = ("Trigger/events", "Trigger/trigger", "Trigger/DST",
"Run/events" , "Run/runInfo")

with tb.open_file(true_out) as true_output_file:
with tb.open_file(PATH_OUT) as output_file:
for table in tables:
assert hasattr(output_file.root, table)
got = getattr( output_file.root, table)
expected = getattr(true_output_file.root, table)
assert_tables_equality(got, expected)
19 changes: 19 additions & 0 deletions invisible_cities/config/valdrada.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
files_in = '$ICDIR/database/test_data/blr_fpga_examples.h5'
file_out = '$ICDIR/database/test_data/valdrada.h5'

compression = 'ZLIB4'
run_number = 8093
detector_db = 'new'
event_range = all

trigger_config = {'coincidence_window':64, 'discard_width':40,
'multipeak':{'q_min':100000, 'time_min':2*mus, 'time_after':800*mus}}

channel_config = {0: {'q_min' :5000, 'q_max' :50000,
'time_min' :2*mus, 'time_max': 40*mus,
'baseline_dev': 10, 'amp_max' : 1000,
'pulse_valid_ext':50*ns},
2: {'q_min' :5000, 'q_max' :50000,
'time_min' :2*mus, 'time_max': 40*mus,
'baseline_dev': 10, 'amp_max' : 1000,
'pulse_valid_ext':50*ns}}
Comment on lines +9 to +19
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add * adc and # time bins as discussed offline.

40 changes: 40 additions & 0 deletions invisible_cities/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ def example_blr_wfs_filename(ICDATADIR):
return os.path.join(ICDATADIR, "blr_examples.h5")


@pytest.fixture(scope='session')
def example_blr_fpga_wfs_filename(ICDATADIR):
return os.path.join(ICDATADIR, "blr_fpga_examples.h5")


@pytest.fixture(scope = 'session',
params = ['electrons_40keV_z250_MCRD.h5'])
def electron_MCRD_file(request, ICDATADIR):
Expand Down Expand Up @@ -699,6 +704,41 @@ def deconvolution_config(ICDIR, ICDATADIR, PSFDIR, config_tmpdir):

return conf, PATH_OUT


@pytest.fixture(scope='function') # Needs to be function as the config dict is modified when running
gonzaponte marked this conversation as resolved.
Show resolved Hide resolved
def trigger_config(ICDIR, ICDATADIR, config_tmpdir):
PATH_IN = os.path.join(ICDATADIR , "blr_fpga_examples.h5")
PATH_OUT = os.path.join(config_tmpdir, "trigger_city_out.h5")
nevt_req = 10
conf = dict(files_in = PATH_IN ,
file_out = PATH_OUT,
event_range = nevt_req,
compression = 'ZLIB4',
print_mod = 1000,
run_number = 8093,
trigger_config = dict(coincidence_window = 64
,discard_width = 40
,multipeak = dict(q_min = 100000
,time_min = 2 *units.mus
,time_after = 800*units.mus)),
channel_config = {0 : dict(q_min = 5000
,q_max = 50000
,time_min = 2 *units.mus
,time_max = 40*units.mus
,baseline_dev = 10
,amp_max = 1000
,pulse_valid_ext = 50*units.ns)
,2 : dict(q_min = 5000
,q_max = 50000
,time_min = 2 *units.mus
,time_max = 40*units.mus
,baseline_dev = 10
,amp_max = 1000
,pulse_valid_ext = 50*units.ns)})
Comment on lines +719 to +737
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add * adc and # time bins as discussed offline.


return conf, PATH_OUT


## To make very slow tests only run with specific option
def pytest_addoption(parser):
parser.addoption(
Expand Down
3 changes: 3 additions & 0 deletions invisible_cities/database/test_data/blr_fpga_examples.h5
Git LFS file not shown
Git LFS file not shown
3 changes: 3 additions & 0 deletions invisible_cities/database/test_data/exact_result_valdrada.h5
Git LFS file not shown
19 changes: 19 additions & 0 deletions invisible_cities/evm/nh5.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,22 @@ class VoxelsTable(tb.IsDescription):
class EventPassedFilter(tb.IsDescription):
event = tb.Int32Col(pos=0)
passed = tb. BoolCol(pos=1)


class TriggerTable(tb.IsDescription):
event = tb.UInt32Col(pos= 0)
pmt = tb.UInt32Col(pos= 1)
trigger_time = tb.UInt32Col(pos= 2)
q = tb.UInt32Col(pos= 3)
width = tb.UInt32Col(pos= 4)
height = tb.UInt32Col(pos= 5)
valid_q = tb.BoolCol (pos= 6)
valid_w = tb.BoolCol (pos= 7)
valid_h = tb.BoolCol (pos= 8)
valid_peak = tb.BoolCol (pos= 9)
valid_all = tb.BoolCol (pos=10)
baseline = tb.Int32Col (pos=11)
max_height = tb.Int32Col (pos=12)
n_coinc = tb.Int32Col (pos=13)
closest_ttime = tb.Int32Col (pos=14)
closest_pmt = tb.Int32Col (pos=15)
Loading