Simulate realistic waveforms

You can also download this tutorial as a Jupyter notebook and a plain Julia source file.

Here we will use an example pet file csv format from LegendTestData corresponding to the Public Inverted Coax.

using LegendGeSim
using Plots

Get inputs from legend-test-data

using LegendTestData

Detector metadata

ldsim_path = joinpath(legend_test_data_path(), "data", "ldsim")

detector_name = "invcoax-metadata"
detector_metadata_filename = joinpath(ldsim_path, detector_name*".json");

Alternatively, enter your own path to a real LEGEND detector JSON

detector_metadata_filename = "path/to/V04545A.json"

PET input file

path_to_pet_file = joinpath(ldsim_path, "single-invcoax-th228-geant4.csv");

Settings

See manual on Field Simulation and Ideal Pulse Simulation for a detailed explanation of environment and simulation settings, as well as the noise model settings

environment_settings = Dict(
    "crystal_temperature_in_K" => 77,
    "medium" => "vacuum",
);

simple settings for point charge simulation with dummy constant impurity

simulation_settings = Dict(
    "method" => "SSD",
    "cached_name" => "", # a non-empty string will cache the simulation results
);
daq_settings = Dict(
    "preamp" => Dict(
        "type" => "generic",
        "t_decay_in_us" => 43, # from V04545A HADES data
        "t_rise_in_ns" => 100, # by eye
        "gain_ADC_eV" => 0.0138, # by eye from V04545A HADES data FEP @ 36100 ADC
        "offset_in_ADC" => 11900, # from V04545A HADES data mean() of baseline
        "noise_sigma_in_keV" => 2 # by eye
    ),
    "fadc" => Dict(
        "type" => "generic",
        "sampling_interval" => 16 # ns, from HADES data
    ),
    "trigger" => Dict(
        "type" => "trapezoidal",
        "window_lengths" => [250,250,250],
        "threshold" => 9 # keV
    ),
    "daq" => Dict(
        "type" => "generic",
        "nsamples" => 3748, # from HADES data
        "baseline_length" => 1770 # by eye from data
    )
);
noise_model = Dict(
    "type" => "sim"
);

Simulate from scratch (pet -> raw)

raw_table = LegendGeSim.simulate_raw(detector_metadata_filename, path_to_pet_file, environment_settings, simulation_settings, daq_settings, noise_model; n_waveforms=10)
Table with 11 columns and 20 rows:
      baseline  channel  energy       ievt  numtraces  packet_id  ⋯
    ┌──────────────────────────────────────────────────────────────
 1  │ 11921.9   1        466.734 keV  2     1.0        0.0        ⋯
 2  │ 11914.8   1        226.265 keV  7     1.0        0.0        ⋯
 3  │ 11916.1   1        82.8155 keV  12    1.0        0.0        ⋯
 4  │ 11911.6   1        484.73 keV   13    1.0        0.0        ⋯
 5  │ 11918.3   1        553.003 keV  17    1.0        0.0        ⋯
 6  │ 11915.7   1        226.476 keV  40    1.0        0.0        ⋯
 7  │ 11917.8   1        2478.94 keV  44    1.0        0.0        ⋯
 8  │ 11912.8   1        338.107 keV  55    1.0        0.0        ⋯
 9  │ 11915.0   1        90.4273 keV  62    1.0        0.0        ⋯
 10 │ 11919.1   1        226.851 keV  64    1.0        0.0        ⋯
 11 │ 11917.3   2        467.897 keV  2     1.0        0.0        ⋯
 12 │ 11916.0   2        224.927 keV  7     1.0        0.0        ⋯
 13 │ 11916.1   2        83.7165 keV  12    1.0        0.0        ⋯
 14 │ 11914.5   2        485.214 keV  13    1.0        0.0        ⋯
 15 │ 11917.0   2        552.744 keV  17    1.0        0.0        ⋯
 16 │ 11914.9   2        225.988 keV  40    1.0        0.0        ⋯
 17 │ 11914.6   2        2478.6 keV   44    1.0        0.0        ⋯
 ⋮  │    ⋮         ⋮          ⋮        ⋮        ⋮          ⋮      ⋱
plot(raw_table.waveform)
Example block output

The file contains double the amount of waveforms (20) compared to what we asked to simulate (n_waveforms = 10). That's because at the pulse simulation level, n+ contact pulses are kept as well. The first 10 entries are p+ contact waveforms.

length(raw_table)
20
plot(raw_table.waveform[1:10], legend=false, title="only p+ contact waveforms")
Example block output

You can save simulated waveforms in a file that can be used later

using LegendHDF5IO
raw_name = "cache/test_100wfs_raw.hdf5"
lh5open(raw_name, "w") do f
    LegendHDF5IO.writedata(f.data_store, "raw", raw_table[1:10])
end

Simulate from pss table in code

Rather that simulating from scratch pet->raw, you may also input an already ready pss table with ideal pulses, and simulate only the pss->raw step, i.e. the DAQ chain simulation

pss_table, pss_truth = LegendGeSim.simulate_pulses(detector_metadata_filename, path_to_pet_file, environment_settings, simulation_settings, noise_model; n_waveforms=10);
[ Info: ---------------------- pet -> stp (stepping info)
Processing file: /home/runner/.julia/artifacts/11b2a7ab47d6f84b449b8f16112d0f3489ec697d/legend-exp-legend-testdata-cd70a8d/data/ldsim/single-invcoax-th228-geant4.csv for detector Public Inverted Coax
[ Info: ...clustering
1411 hits before clustering
  0.001162 seconds (16.68 k allocations: 1.497 MiB)
244 hits after clustering
[ Info: ...removing hits with no energy deposits
[ Info: ...removing events outside of the detector
[ Info: Simulation method: SSD
┌ Warning: No crystal metadata path given. Simulation with dummy constant impurity density.
@ LegendGeSim ~/work/LegendGeSim.jl/LegendGeSim.jl/src/pss.jl:74
[ Info: ---------------------- stp -> pss (ideal pulses)
[ Info: _||_||_||_ Simulate detector
[ Info: Simulating with SSD from scratch for given settings
[ Info: DL = 0mm
┌ Warning: You did not provide operating voltage in environment settings -> taking recommended voltage from metadata
@ LegendGeSim ~/work/LegendGeSim.jl/LegendGeSim.jl/src/legend_detector_to_ssd.jl:45
[ Info: Simulating at 3800.0V
...electric potential
...electric field
...weighting potential 1
...weighting potential 2
[ Info: //\//\//\ Adding fano noise
[ Info: DL = 0mm
┌ Warning: You did not provide operating voltage in environment settings -> taking recommended voltage from metadata
@ LegendGeSim ~/work/LegendGeSim.jl/LegendGeSim.jl/src/legend_detector_to_ssd.jl:45
[ Info: Simulating at 3800.0V
[ Info: ~.~.~.~.~ Simulate charge pulses
[ Info: ~.~.~ SolidStateDetectors
[ Info: Detector has 2 contacts
[ Info: Table has 10 physics events (23 single charge depositions).
[ Info: Generating waveforms...
plot(pss_table.waveform[1:10])
Example block output
raw_table1 = LegendGeSim.pss_to_raw(pss_table, pss_truth, daq_settings, noise_model)
Table with 11 columns and 20 rows:
      baseline  channel  energy       ievt  numtraces  packet_id  ⋯
    ┌──────────────────────────────────────────────────────────────
 1  │ 11915.8   1        467.8 keV    2     1.0        0.0        ⋯
 2  │ 11913.3   1        227.318 keV  7     1.0        0.0        ⋯
 3  │ 11913.6   1        83.8068 keV  12    1.0        0.0        ⋯
 4  │ 11915.7   1        484.61 keV   13    1.0        0.0        ⋯
 5  │ 11916.6   1        554.221 keV  17    1.0        0.0        ⋯
 6  │ 11918.0   1        227.34 keV   40    1.0        0.0        ⋯
 7  │ 11912.3   1        2479.98 keV  44    1.0        0.0        ⋯
 8  │ 11921.8   1        337.837 keV  55    1.0        0.0        ⋯
 9  │ 11917.5   1        89.2925 keV  62    1.0        0.0        ⋯
 10 │ 11914.7   1        226.288 keV  64    1.0        0.0        ⋯
 11 │ 11916.7   2        467.631 keV  2     1.0        0.0        ⋯
 12 │ 11914.6   2        226.594 keV  7     1.0        0.0        ⋯
 13 │ 11916.6   2        82.8852 keV  12    1.0        0.0        ⋯
 14 │ 11913.0   2        485.072 keV  13    1.0        0.0        ⋯
 15 │ 11918.8   2        553.952 keV  17    1.0        0.0        ⋯
 16 │ 11916.4   2        226.341 keV  40    1.0        0.0        ⋯
 17 │ 11920.0   2        2479.71 keV  44    1.0        0.0        ⋯
 ⋮  │    ⋮         ⋮          ⋮        ⋮        ⋮          ⋮      ⋱
plot(
    plot(pss_table.waveform[1:10]),
    plot(raw_table1.waveform[1:10]),
    size=(800,400)
)
Example block output

Save the pss file for the next section

using LegendHDF5IO
pss_name = "cache/test_100wfs_pss.hdf5"
lh5open(pss_name, "w") do f
    LegendHDF5IO.writedata(f.data_store, "pss/pss", pss_table[1:10])
    LegendHDF5IO.writedata(f.data_store, "pss/truth", pss_truth[1:10])
end

Simulate from pre-saved pss hdf5 file

Rather than simulating form scratch pet->raw, you may input the name of a pre-saved pss file containing pss and pss truth information

raw_table2 = LegendGeSim.pss_to_raw(pss_name, daq_settings, noise_model)
Table with 11 columns and 10 rows:
      baseline  channel  energy       ievt  numtraces  packet_id  ⋯
    ┌──────────────────────────────────────────────────────────────
 1  │ 11912.6   1        468.364 keV  2     1.0        0.0        ⋯
 2  │ 11916.8   1        225.726 keV  7     1.0        0.0        ⋯
 3  │ 11918.8   1        82.3564 keV  12    1.0        0.0        ⋯
 4  │ 11918.2   1        484.489 keV  13    1.0        0.0        ⋯
 5  │ 11917.6   1        553.34 keV   17    1.0        0.0        ⋯
 6  │ 11916.6   1        227.08 keV   40    1.0        0.0        ⋯
 7  │ 11915.6   1        2480.69 keV  44    1.0        0.0        ⋯
 8  │ 11919.7   1        338.041 keV  55    1.0        0.0        ⋯
 9  │ 11913.0   1        91.4586 keV  62    1.0        0.0        ⋯
 10 │ 11919.3   1        225.429 keV  64    1.0        0.0        ⋯

This page was generated using Literate.jl.