ToA information in Timepix4

Hello,

I just started using allpix2 with an externally simulated (Geant4) root file for a setup with 7 Timepix4 chips and 500 um silicon sensors for a project of studying antiproton annihilations.
In order to perform a proper data analysis with track reconstruction I would need to precisely get the ToA of my signals. I have read in previous posts that it is suggested to use a combination of [TransientPropagation] + [PulseTransfer] + [CSADigitizer] to get the best results but I run into some issues.

Since for the TransientPropagation I would need to know the weighting potential, I have for now used the [ProjectionPropagation] instead.
For our experiment previously the [ADTreeWriter] module was used to create root files which I adjusted a bit to now also include the ToA as hit.getLocalTime().
The combination of [ProjectionPropagation] + [PulseTransfer] + [DefaultDigitizer] gives me the expected root files, but when I try to use the [CSADigitizer] instead, my root files remain empty.
Do I need to use a different function in order to get the ToA from the CSADigitizer?
If I want to use the TransientPropagation how could I get a weighting potential map for the Tpx4?

I will attach my .conf file (with the working DefaultDigitizer and commented CSADigitizer) and the adjusted ADTreeWriterModule.cpp as a txt file.

Cheers,
Viktoria

tpx_digit.conf (1.4 KB)
ADTreeWriterModule.txt (5.1 KB)

Hi @krixikraxi

there are several levels of complexity for time-resolved simulations, the ones you looked at so far are

  • the most complete: TransientPropagation will create a full current pulse simulation in the sensor, and subsequent modules will then be able to really see when the threshold is crossed for a given current pulse. This requires the Ramo potential as you have correctly stated.
  • the most coarse: PropjectionPropagation will calculate the approximate drift time of charge carriers in a linear field, put them to the surface and then move them there in one step. Subsequent modules have only access to the “arrival time”, and PulseTransfer will build a “pseudo-pulse” from the arrival times.

Given that you are using a linear electric field (in a presumably reasonably thick planar silicon sensor) the latter approach should be enough. For a first estimate, there is no need for using the CSADigitizer, the DefaultDigitizer should suffice.

Have you already had a look at the resulting time distributions?

Best regards,
Simon

Hi @simonspa

thanks for your quick reply!
Here is a comparison of the time simulated in Geant4 (left) and the ToA I get:

Cheers,
Viktoria

Hi again,

I have another question regarding the ToA.
Since I am not using the CSADigitizer I could not set the clock_bin_toa - so the ToA should be in nanoseconds and not in clock cycles, right?
Should the time histograms from my previous post then not look more similar?

Cheers,
Viktoria

Hi @krixikraxi

yes, at least the histogram from Allpix Squared should be in nanoseconds, for your Geant4 simulation I can of course only guess. But since you don’t set unit_time in the DepositionReader, also this should be time. The difference between the two could be local to global time references, i.e. begin of the event vs. begin of the detector timeframe - see here: Simulation Geometry | Allpix Squared

Best,
Simon

Hi @simonspa

somehow it makes no difference whether I do hit.getGlobalTime() or hit.getLocalTime() - I always get the same distribution. Is it correct to get it from the PixelHit or should I get it from somewhere else?

Best,
Viktoria

Hi @krixikraxi

if you provide your configs and maybe make ane xample data file available, I can check next week if I find out what’s going on… :slight_smile:

Simon

Hi @simonspa

Sorry for the late reply!
Here are all the files I use: allpix_test – Google Drive
It is run like this bash ./run.sh output-test-Pb-250eV-FTFP-BERT-EMZ_StepFunction_100nm-cut1eV.root Geant4 Pb 0

Thank you,
Viktoria

Hi @simonspa

I still have not figured out my issue with the timing. I adjusted the G4 simulation to also have a global time leaf in my root file which I use for the allpix. Then I tried to generate the ToA once from the normal time and once from the global time - but the ToA distribution I get is still the same.

Cheers,
Viktoria

Hi @krixikraxi

sorry this took so long, I finally found the time to look at your example and configurations. It took me while to understand the situation, but you actually head-on hit a bug… :slight_smile:

  • In Allpix Squared, global and local time are defined like follows:

    The global reference for time measurements is the beginning of the event, i.e. the start of the particle tracking through the setup. The local time reference is the time of entry of the first primary particle of the event into the sensor.

    Simulation Geometry | Allpix Squared

  • The DepositionReader module takes the first appearance of a MCParticle as the local reference time for calculating the deposition times in the sensor.

  • In the PulseTransfer we then convert the generated and propagated charge carriers into a pulse and add that (including its references the MCParticles) to the PixelCharge object - and here things go wrong. Look at the following code:

    // Store the MC particle references
    for(const auto& mc_particle : unique_particles) {
        // Local and global time are set as the earliest time found among the MCParticles:
        if(mc_particle != nullptr) {
            const auto* primary = mc_particle->getPrimary();
            local_time_ = std::min(local_time_, primary->getLocalTime());
            global_time_ = std::min(global_time_, primary->getGlobalTime());
        }
        mc_particles_.emplace_back(mc_particle);
    }
    

    …it looks innocent enough - but if both local_time_ and global_time_ were initialized to zero, they will always and forever remain zero. :confused: So what you are seeing is then just the ToA relative to the begin of the event, so relative to when the first MCParticle entered the sensor…

I fixed this in this merge request and invite you to try it out. Now the reference times are initialized as infinity and then reduced to whatever value we get from the MCParticles. In case we don’t have them or don’t get valid data, they are set back to zero (current case).

This fix will be released in the coming days in Allpix Squared 3.0.1.

Please let me know if now your simulations make more sense! :slight_smile:

SImon

Hi @simonspa
I’m glad I could find a bug for you :grinning:

I just wanted to try it with the 3.0.1, but I run into errors when I try to make the ADTreeWriter. Is it possible that the CMakeLists.txt should be changed for the new release?

Thank you!

Viktoria

Hi @krixikraxi

I updated it to request version 3.0 - but is should work anyway. I think you have to remove the build folder and re-compile - but other than that it just just work :tm:

Let me know if you continue to have issues!

Simon

Hi @simonspa

there was another error with linking libraries, but I could fix it by manually linking them.
The global time now looks as expected, the local time still has the same structure as before, but could be that’s just what it looks like?

Also, is there a possibility to have the global time with a sub-ns resolution?

Thank you!

Viktoria

Hi @krixikraxi

yes, I didn’t expect the local time to change, really, because that is always with respect to the entry of the particle into the sensor. To me it therefore makes sense that you get roughly the same arrival time of the signal in the sensor with respect to the particle arrival.

The global time in Allpix Squared is stored as a double, so you have a lot more precision. However, looking at your ADTrees, you are currently only storing the local time - if you let me know how you would like this to be stored, I can just update that module. We could add another branch?

Simon

Hi @simonspa

okay, then all should be fine now, thanks again for looking into it!

I am already using an adjusted version of the ADTreeWriter (with branches adjusted to Tpx4 structure instead of Tpx3), but I just saw that I had still called the GlobalTime as an UInt_t instead of Double_t, that fixed the resolution issue :sweat_smile:

I have also tried to implement the track_id as was discussed in this thread

because I wanted to check if my clustering algorithm is properly differentiating the tracks/particles.
I added the lines as shown in the thread/Gitlab links into the MCParticle class and incremented ClassDefOverride(MCParticle, 10); to ClassDefOverride(MCParticle, 11); (as there are now already 10), but when I try to call it the same way as the ParticleID I still get only zeros. Do you have an idea what could be the issue there?

auto particles = hit.getPrimaryMCParticles();
auto mcparticle = (particles.empty() ? nullptr : particles.front());
        data_.pdg_codes_ = (mcparticle != nullptr ? mcparticle->getParticleID() : 0);
        data_.track_ = (mcparticle != nullptr ? mcparticle->getParticleTrackID() : 0);

Here is the ADTreeWriter I currently use: CERNBox

Thanks and all the best,
Viktoria

I just gave you access to my repository, so feel free to update everything there to your liking and with your code - I wrote this for your group only anyways :slight_smile:

For the track_id - could you share what exactly you have changed? Maybe I can also come up with a better way of including this information - or maybe in the meantime this has been solved differently (that thread is from Nov’21…)

Best,
Simon

Thanks!

I did all the changes that are mentioned here for MCParticle.cpp and MCParticle.hpp:

I put the two files here: CERNBox

Cheers,
Viktoria

Hi @krixikraxi

if you really only modified those two files, then I expect them to always be zero (or actually -1). You also need to collect the information from the module that generates these objects, so the following changes:

--- a/src/modules/DepositionReader/DepositionReaderModule.cpp
+++ b/src/modules/DepositionReader/DepositionReaderModule.cpp
@@ -221,6 +221,7 @@ void DepositionReaderModule::run(Event* event) {
     std::map<std::shared_ptr<Detector>, std::vector<double>> mc_particle_time;
     std::map<std::shared_ptr<Detector>, std::vector<int>> mc_particle_parent;
     std::map<std::shared_ptr<Detector>, std::vector<unsigned int>> mc_particle_charge;
+    std::map<std::shared_ptr<Detector>, std::vector<int>> mc_particle_trackid;
 
     std::map<std::shared_ptr<Detector>, std::vector<int>> particles_to_deposits;
     std::map<std::shared_ptr<Detector>, std::map<int, size_t>> track_id_to_mcparticle;
@@ -309,6 +310,7 @@ void DepositionReaderModule::run(Event* event) {
             mc_particle_time[detector].push_back(time);
             mc_particle_code[detector].push_back(pdg_code);
             mc_particle_parent[detector].push_back(parent_id);
+            mc_particle_trackid[detector].push_back(track_id);
             mc_particle_charge[detector].push_back(charge);
             track_id_to_mcparticle[detector][track_id] = (mc_particle_start[detector].size() - 1);
         } else {
@@ -345,9 +347,10 @@ void DepositionReaderModule::run(Event* event) {
 
             auto pdg_code = mc_particle_code[detector].at(i);
             auto time = mc_particle_time[detector].at(i);
+            auto track_id = mc_particle_trackid[detector].at(i);
 
             mc_particles.emplace_back(
-                start_local, start_global, end_local, end_global, pdg_code, time - time_reference, time);
+                start_local, start_global, end_local, end_global, pdg_code, time - time_reference, time, track_id);
             // Count electrons and holes:
             mc_particles.back().setTotalDepositedCharge(2 * mc_particle_charge[detector].at(i));
         }

Then it should properly store the track IDs read from your G4 input files.

Cheers,
Simon

Hi @simonspa

yes, it was actually always -1 :sweat_smile:
Now it’s working, thanks for your help!

Cheers,
Viktoria

1 Like