LDMX Software
ldmx::PulseFitter Class Reference

Fits a pulse shape to the ADC samples of a single readout channel to extract hit amplitude and time. More...

#include <PulseFitter.h>

Classes

struct  FitResult
 

Public Member Functions

 PulseFitter (const PulseShape &shape, double t0_offset_ns, double sampling_interval_ns, double pedestal_adc, double noise_sigma_adc, double t_scan_min_ns=-50.0, double t_scan_max_ns=150.0, double t_scan_step_ns=1.0)
 
FitResult fit (const std::vector< short > &samples) const
 Fit the pulse to the given ADC sample vector.
 
FitResult fitFixed (const std::vector< short > &samples, double T) const
 Amplitude only fit with the hit time T known, e.g.
 

Private Member Functions

std::pair< double, double > evalAtT (const std::vector< short > &samples, double T) const
 Evaluate χ²(T) and the corresponding best-fit amplitude.
 

Private Attributes

const PulseShape * shape_
 
double t0_offset_ns_
 
double sampling_interval_ns_
 
double pedestal_adc_
 
double inv_sigma2_
 1/σ², pre-computed.
 
double t_scan_min_ns_
 
double t_scan_max_ns_
 
double t_scan_step_ns_
 

Detailed Description

Fits a pulse shape to the ADC samples of a single readout channel to extract hit amplitude and time.

Algorithm

With N ADC samples and 2 free parameters (amplitude A and hit time T), the chi-squared is:

χ²(T) = Σ_i [(S_i − ped − A(T)·f(t_i − T)) / σ]²

where t_i = t0_offset + i·Δt are the sample times and f is the pulse shape. For each candidate T, A is solved analytically:

A(T) = Σ_i (S_i − ped)·f(t_i − T) / Σ_i f(t_i − T)²

A 1-D scan over T followed by a parabolic refinement near the minimum gives the fitted values. This is O(n_scan × N_samples) per strip and runs in < 1 µs.

The fit result t0 is defined as T − t0_offset, i.e. the hit arrival time relative to sample 0. A value of t0 = tp (peaking time) means the pulse peak coincides with sample 0.

Timing convention

Sample i is taken at time t_i = t0_offset_ns + i · sampling_interval_ns The pulse shape is evaluated at f(t_i − T) where T is the absolute hit arrival time in the same frame. The output t0 = T is stored directly so the caller can subtract their own reference as needed.

Definition at line 40 of file PulseFitter.h.

Constructor & Destructor Documentation

◆ PulseFitter()

ldmx::PulseFitter::PulseFitter ( const PulseShape & shape,
double t0_offset_ns,
double sampling_interval_ns,
double pedestal_adc,
double noise_sigma_adc,
double t_scan_min_ns = -50.0,
double t_scan_max_ns = 150.0,
double t_scan_step_ns = 1.0 )
Parameters
shapePulse shape function (owned externally).
t0_offset_nsTime of sample 0 in the hit-time frame [ns].
sampling_interval_nsInterval between samples [ns].
pedestal_adcPer-sample pedestal [ADC counts].
noise_sigma_adcPer-sample noise RMS [ADC counts], used for χ².
t_scan_min_nsLower bound of the T scan range [ns].
t_scan_max_nsUpper bound of the T scan range [ns].
t_scan_step_nsStep size of the coarse T scan [ns].

Definition at line 8 of file PulseFitter.cxx.

12 : shape_(&shape),
13 t0_offset_ns_(t0_offset_ns),
14 sampling_interval_ns_(sampling_interval_ns),
15 pedestal_adc_(pedestal_adc),
16 inv_sigma2_(1.0 / (noise_sigma_adc * noise_sigma_adc)),
17 t_scan_min_ns_(t_scan_min_ns),
18 t_scan_max_ns_(t_scan_max_ns),
19 t_scan_step_ns_(t_scan_step_ns) {}
double inv_sigma2_
1/σ², pre-computed.
Definition PulseFitter.h:95

Member Function Documentation

◆ evalAtT()

std::pair< double, double > ldmx::PulseFitter::evalAtT ( const std::vector< short > & samples,
double T ) const
private

Evaluate χ²(T) and the corresponding best-fit amplitude.

Definition at line 25 of file PulseFitter.cxx.

26 {
27 const int n = static_cast<int>(samples.size());
28
29 // Evaluate pulse shape at each sample time.
30 // t_i = t0_offset + i * dt → argument to shape = t_i - T
31 double sum_sf = 0.0; // Σ (S_i - ped) * f_i
32 double sum_f2 = 0.0; // Σ f_i²
33
34 for (int i = 0; i < n; ++i) {
35 const double t_arg = t0_offset_ns_ + i * sampling_interval_ns_ - T;
36 const double fi = shape_->eval(t_arg);
37 const double si = static_cast<double>(samples[i]) - pedestal_adc_;
38 sum_sf += si * fi;
39 sum_f2 += fi * fi;
40 }
41
42 // Degenerate: pulse shape is zero everywhere in the window.
43 if (sum_f2 < 1.0e-12) {
44 return {std::numeric_limits<double>::max(), 0.0};
45 }
46
47 const double amplitude = sum_sf / sum_f2;
48
49 // Compute chi2 for this (T, amplitude) pair.
50 double chi2 = 0.0;
51 for (int i = 0; i < n; ++i) {
52 const double t_arg = t0_offset_ns_ + i * sampling_interval_ns_ - T;
53 const double fi = shape_->eval(t_arg);
54 const double si = static_cast<double>(samples[i]) - pedestal_adc_;
55 const double resid = si - amplitude * fi;
56 chi2 += resid * resid * inv_sigma2_;
57 }
58
59 return {chi2, amplitude};
60}
virtual double eval(double t) const =0
Evaluate the peak-normalised pulse amplitude at time t [ns] after charge arrival.

References ldmx::PulseShape::eval(), and inv_sigma2_.

Referenced by fit(), and fitFixed().

◆ fit()

PulseFitter::FitResult ldmx::PulseFitter::fit ( const std::vector< short > & samples) const

Fit the pulse to the given ADC sample vector.

Parameters
samplesADC values (one per sample, any integer type cast to short).

Definition at line 66 of file PulseFitter.cxx.

67 {
68 FitResult result;
69 result.ndf = static_cast<int>(samples.size()) - 2;
70
71 // Quick sanity: check that at least one sample is above pedestal.
72 bool any_signal = false;
73 for (auto s : samples) {
74 if (static_cast<double>(s) > pedestal_adc_) {
75 any_signal = true;
76 break;
77 }
78 }
79 if (!any_signal) {
80 return result; // converged = false
81 }
82
83 // -------------------------------------------------------------------
84 // Coarse scan over T to find the approximate minimum.
85 // -------------------------------------------------------------------
86 double best_chi2 = std::numeric_limits<double>::max();
87 double best_t = t_scan_min_ns_;
88 double best_amp = 0.0;
89
90 // Keep track of the three-point neighbourhood for parabolic refinement.
91 double t_lo = t_scan_min_ns_, chi2_lo = std::numeric_limits<double>::max();
92 double t_hi = t_scan_min_ns_, chi2_hi = std::numeric_limits<double>::max();
93
94 double t_prev = t_scan_min_ns_;
95 double chi2_prev = std::numeric_limits<double>::max();
96
97 for (double t = t_scan_min_ns_; t <= t_scan_max_ns_; t += t_scan_step_ns_) {
98 auto [chi2, amp] = evalAtT(samples, t);
99
100 if (chi2 < best_chi2) {
101 // Update neighbourhood: the previous point becomes T_lo.
102 t_lo = t_prev;
103 chi2_lo = chi2_prev;
104 best_chi2 = chi2;
105 best_t = t;
106 best_amp = amp;
107 // T_hi is the next point, forget one found for an earlier minimum
108 chi2_hi = std::numeric_limits<double>::max();
109 t_hi = t;
110 } else if (t_prev == best_t) {
111 // First point to the right of the minimum.
112 t_hi = t;
113 chi2_hi = chi2;
114 }
115
116 t_prev = t;
117 chi2_prev = chi2;
118 }
119
120 // -------------------------------------------------------------------
121 // Parabolic refinement around the minimum if neighbours are available.
122 // -------------------------------------------------------------------
123 if (t_lo < best_t && t_hi > best_t && chi2_lo > best_chi2 &&
124 chi2_hi > best_chi2) {
125 // Fit a parabola through (T_lo, chi2_lo), (best_T, best_chi2), (T_hi,
126 // chi2_hi).
127 const double d1 = best_t - t_lo;
128 const double d2 = t_hi - best_t;
129 const double dc1 = chi2_lo - best_chi2;
130 const double dc2 = chi2_hi - best_chi2;
131 // Vertex of parabola: ΔT = (d2²·dc1 - d1²·dc2) / (2·(d1·dc2 + d2·dc1))
132 const double denom = 2.0 * (d1 * dc2 + d2 * dc1);
133 if (std::abs(denom) > 1.0e-12) {
134 const double delta = (d2 * d2 * dc1 - d1 * d1 * dc2) / denom;
135 // Only accept the refinement if it stays within the bracket.
136 if (std::abs(delta) < std::max(d1, d2)) {
137 const double t_refined = best_t + delta;
138 auto [chi2_ref, amp_ref] = evalAtT(samples, t_refined);
139 if (chi2_ref < best_chi2) {
140 best_t = t_refined;
141 best_amp = amp_ref;
142 best_chi2 = chi2_ref;
143 }
144 }
145 }
146 }
147
148 result.amplitude = best_amp;
149 result.t0 = best_t;
150 result.chi2 = best_chi2;
151 result.converged = true;
152 return result;
153}
std::pair< double, double > evalAtT(const std::vector< short > &samples, double T) const
Evaluate χ²(T) and the corresponding best-fit amplitude.

References ldmx::PulseFitter::FitResult::amplitude, ldmx::PulseFitter::FitResult::chi2, ldmx::PulseFitter::FitResult::converged, evalAtT(), ldmx::PulseFitter::FitResult::ndf, and ldmx::PulseFitter::FitResult::t0.

Referenced by ecal::EcalRecProducer::produce(), and tracking::reco::SiStripWaveformFitProcessor::produce().

◆ fitFixed()

PulseFitter::FitResult ldmx::PulseFitter::fitFixed ( const std::vector< short > & samples,
double T ) const

Amplitude only fit with the hit time T known, e.g.

from another detector.

Parameters
samplesADC values (one per sample).
THit time in the same frame as t0_offset [ns].

Definition at line 159 of file PulseFitter.cxx.

160 {
161 FitResult result;
162 result.ndf = static_cast<int>(samples.size()) - 1;
163 auto [chi2, amp] = evalAtT(samples, T);
164 result.amplitude = amp;
165 result.t0 = T;
166 result.chi2 = chi2;
167 result.converged = chi2 < std::numeric_limits<double>::max();
168 return result;
169}

References ldmx::PulseFitter::FitResult::amplitude, ldmx::PulseFitter::FitResult::chi2, ldmx::PulseFitter::FitResult::converged, evalAtT(), ldmx::PulseFitter::FitResult::ndf, and ldmx::PulseFitter::FitResult::t0.

Referenced by ecal::EcalRecProducer::produce().

Member Data Documentation

◆ inv_sigma2_

double ldmx::PulseFitter::inv_sigma2_
private

1/σ², pre-computed.

Definition at line 95 of file PulseFitter.h.

Referenced by evalAtT().

◆ pedestal_adc_

double ldmx::PulseFitter::pedestal_adc_
private

Definition at line 94 of file PulseFitter.h.

◆ sampling_interval_ns_

double ldmx::PulseFitter::sampling_interval_ns_
private

Definition at line 93 of file PulseFitter.h.

◆ shape_

const PulseShape* ldmx::PulseFitter::shape_
private

Definition at line 91 of file PulseFitter.h.

◆ t0_offset_ns_

double ldmx::PulseFitter::t0_offset_ns_
private

Definition at line 92 of file PulseFitter.h.

◆ t_scan_max_ns_

double ldmx::PulseFitter::t_scan_max_ns_
private

Definition at line 97 of file PulseFitter.h.

◆ t_scan_min_ns_

double ldmx::PulseFitter::t_scan_min_ns_
private

Definition at line 96 of file PulseFitter.h.

◆ t_scan_step_ns_

double ldmx::PulseFitter::t_scan_step_ns_
private

Definition at line 98 of file PulseFitter.h.


The documentation for this class was generated from the following files: