Optical crosstalk is a interaction between pixels on the same array. When an avalanche is triggered on one pixel it emits secondary photons that can trigger another pixel on the same chip. This secondary photon emission is proportional to the avalanche current whose duration is typically < 20ns in SPADs using AQCs. Hence, when a crosstalk event occurs, the second pixels is triggered with a delay < 20ns. We can estimate the crosstalk by "coincidence counting", that is counting the number of photons C arriving in two pixels within a short time window Δt (e.g. 25-50ns). The number of coincident events due to uncorrelated dark counts can be computed from Poisson statistics. Then, the number of crosstalk events is simply the total coincidence counts minus the coincidence due to uncorrelated counts. The crosstalk probability is the number of coincidences divided by the total counts.
Reference:
Given the random variable X∼Poiss{Λ}, where Λ=λΔt. We ask, what is the probability of having at least one count in a time window δt?
P{X≥1}=1−P{X=0}=1−eΛ→Λ→0ΛHence, for two random variables XA∼Poiss{ΛA} and XB∼Poiss{ΛB}, the propbability of having at least one count in each variable in a time Δt is:
P{XA≥1}P{XB≥1}=(1−e−ΛA)(1−e−ΛB)≈(λAΔt)(λBΔt)forΔt≪λ−1In a measurement of duration T, number of "coincidences" Cu is the number of times both variables have at least one count in a window Δt
Cu=P{XA≥1}P{XB≥1}TΔt≈λAλBTΔtforΔt≪λ−1Given a measurement of duration T, with total counts NA and NB in pixels A and B. If C are the total number of coincidence in windows of duration Δt, then the coincidence Cc due to crosstalk are:
Cc=C−Cu(1)where Cu are the number of coincidences due to Poisson statistics, i.e. the coincidences we would have if the two pixels were uncorrelated.
Cu=[1−exp(−NA−CcT)][1−exp(−NB−CcT)]TΔtThe expression of Cu can be replaced eq. (1) and then solved for Cc (a numerical iterative solution is straightforward). For simplicity, we could assume Cc≪NA,NB obtaining:
Cu=[1−exp(−NAT)][1−exp(−NBT)]TΔtIn either cases, the probability of crosstalk is:
Pc=CcNA+NB−CcThe counts Cc are independetent events and thus are Poisson distributed. The standard deviation of Cc is then Std{Cc}=√Cc, and the standard deviation of Pc is:
Std{Pc}=√CcNA+NB−Ccdef coincidence_py(timestamps1, timestamps2):
"""Pure python coincidence counting."""
coinc = 0
i1, i2 = 0, 0
while (i1 < timestamps1.size) and (i2 < timestamps2.size):
if timestamps1[i1] == timestamps2[i2]:
coinc += 1
i1 += 1
i2 += 1
elif timestamps1[i1] > timestamps2[i2]:
i2 += 1
elif timestamps1[i1] < timestamps2[i2]:
i1 += 1
return coinc
%load_ext Cython
%%cython
cimport numpy as np
def coincidence(np.int64_t[:] timestamps1, np.int64_t[:] timestamps2):
"""Cython coincidence counting."""
cdef np.int64_t coinc, i1, i2, size1, size2
size1 = timestamps1.size
size2 = timestamps2.size
coinc = 0
i1, i2 = 0, 0
while i1 < size1 and i2 < size2:
if timestamps1[i1] == timestamps2[i2]:
coinc += 1
i1 += 1
i2 += 1
elif timestamps1[i1] > timestamps2[i2]:
i2 += 1
elif timestamps1[i1] < timestamps2[i2]:
i1 += 1
return coinc
def crosstalk_probability(t1, t2, tol=1e-6, max_iter=100):
"""Estimate crosstalk probability between two pixels in a SPAD array.
Given two input arrays of timestamps `t1` and `t2`, estimate
the crosstalk probability using Poisson statistics without
approximations.
Arguments:
t1, t2 (array of ints): arrays of timestamps from DCR measurements
for the two pixels to be measured. Timestamps need to be
integers and coincidences are detected when values in the two
arrays are equal. These arrays need to be rescaled, if
coincidence need to be computed on a delta t larger than \
a single timestamp unit.
tol (float): tollerance for iterative equasion solution
max_iter (int): max iterations used to solve the equation
Returns:
A 3-element tuple:
- crosstalk probability
- crosstalk probability standard deviation
- number of iterations used for the estimation.
"""
T = (max((t1.max(), t2.max())) - min((t1.min(), t2.min())))
C = coincidence(t1, t2)
# Find C_c by solving eq. (1) iteratively
C_c, C_u_prev = 0, 0
for i in range(max_iter):
C_u = ((1 - np.exp(-(t1.size - C_c)/T)) *
(1 - np.exp(-(t2.size - C_c)/T)) * T)
C_c = C - C_u
if np.abs(C_u - C_u_prev) < tol:
break
C_u_prev = C_u
P_c = C_c / (t1.size + t2.size - C_c)
sigma = np.sqrt(C_c) / (t1.size + t2.size - C_c)
return P_c, sigma, i
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
Simulation parameters:
λ_A = 1000 # cps
λ_B = 2000 # cps
T = 600 # s
delta_t = 50e-9 # s
P_c = 0.05
From theory, the number of coincidences are:
C_u = λ_A * λ_B * T * delta_t
C_u
60.0
np.sqrt(C_u)
7.745966692414834
Let's simulate timestamps for two uncorrelated Poisson processes:
dt = np.random.exponential(scale=1/λ_A, size=10**6)
dt.mean(), dt.sum()
(0.0010001265879312606, 1000.1265879312606)
np.random.seed(1)
t_A = np.cumsum(np.random.exponential(scale=1/λ_A, size=λ_A * T * 2))
assert t_A.max() > T
t_A = t_A[t_A < 600]
t_A = (t_A / delta_t).astype('int64')
t_B = np.cumsum(np.random.exponential(scale=1/λ_B, size=λ_B * T * 2))
assert t_B.max() > T
t_B = t_B[t_B < 600]
t_B = (t_B / delta_t).astype('int64')
The empirical coincidences are:
C_u_sim = coincidence(t_A, t_B)
C_u_sim
62
assert C_u - 3*np.sqrt(C_u) < C_u_sim < C_u + 3*np.sqrt(C_u)
Let's repeat the simulation N times to be sure that the estimated coincidence falls within the espected error range all the times:
N = 500
fail = 0
for _ in range(N):
t_A = np.cumsum(np.random.exponential(scale=1/λ_A, size=λ_A * T * 2))
assert t_A.max() > T
t_A = t_A[t_A < T]
t_A = (t_A / delta_t).astype('int64')
t_B = np.cumsum(np.random.exponential(scale=1/λ_B, size=λ_B * T * 2))
assert t_B.max() > T
t_B = t_B[t_B < T]
t_B = (t_B / delta_t).astype('int64')
C_u_sim = coincidence(t_A, t_B)
if C_u < C_u_sim - 3*np.sqrt(C_u_sim) or C_u > C_u_sim + 3*np.sqrt(C_u_sim):
fail += 1
print('>>> In %d simulations, the number of coincincence was outside the error range %d times'
% (N, fail/N))
>>> In 500 simulations, the number of coincincence was outside the error range 0 times
For further information of confidence intervals for Poisson distribution estimators see:
Here we simulate two Poisson processes plus crosstalk, to check that the estimated crosstalk is consistent with the ground truth.
λ_A = 1000 # cps
λ_B = 1550 # cps
T = 1200 # s
delta_t = 50e-9 # s
P_c = 0.1
np.random.seed(1)
t_A = np.cumsum(np.random.exponential(scale=1/λ_A, size=λ_A * T * 2))
assert t_A.max() > T
t_A = t_A[t_A < 600]
t_A = (t_A / delta_t).astype('int64')
t_ct_AB = t_A[np.random.rand(t_A.size) <= P_c]
t_B = np.cumsum(np.random.exponential(scale=1/λ_B, size=λ_B * T * 2))
assert t_B.max() > T
t_B = t_B[t_B < 600]
t_B = (t_B / delta_t).astype('int64')
t_ct_BA = t_B[np.random.rand(t_B.size) <= P_c]
t_B = np.hstack([t_B, t_ct_AB])
t_B.sort()
t_A = np.hstack([t_A, t_ct_BA])
t_A.sort()
P_c_est, P_c_err, i = crosstalk_probability(t_A, t_B, delta_t)
P_c_est*100, P_c_err*300, i
(9.9967084810755544, 0.076659160092492767, 4)
Let's repeat the simulation N times to obtain a distribution of estimated crosstalk values:
λ_A = 1000 # cps
λ_B = 300 # cps
T = 1200 # s
delta_t = 50e-9 # s
P_c = 0.05
N = 100
P = []
I = 0
for _ in range(N):
t_A = np.cumsum(np.random.exponential(scale=1/λ_A, size=λ_A * T * 2))
assert t_A.max() > T
t_A = t_A[t_A < T]
t_A = (t_A / delta_t).astype('int64')
t_ct_AB = t_A[np.random.rand(t_A.size) <= P_c]
t_B = np.cumsum(np.random.exponential(scale=1/λ_B, size=λ_B * T * 2))
assert t_B.max() > T
t_B = t_B[t_B < T]
t_B = (t_B / delta_t).astype('int64')
t_ct_BA = t_B[np.random.rand(t_B.size) <= P_c]
t_B = np.hstack([t_B, t_ct_AB])
t_B.sort()
t_A = np.hstack([t_A, t_ct_BA])
t_A.sort()
P_c_est, P_c_err, i = crosstalk_probability(t_A, t_B)
I += i
P.append(P_c_est*100)
I/N
3.0
plt.hist(P, range=(96*P_c, 104*P_c), bins=60, label='Estimator');
plt.axvline(P_c*100, ls='--', color='k', label='True value')
plt.xlabel('Crosstalk probability (%)')
plt.title('Simulation of crosstalk estimator accuracy');
NOTE The crosstalk estimator is well centered around the true value.