Coincidences and timing jitter (delta-t)¶
A daplis-style pipeline for coincidence / timing-jitter analysis of the Kelpie
ORT (timestamp) program, built on three layers:
unpackdecodes each.binfile into the per-frame, per-pixel TDC code (time_series, the MATLABtime_mat).calc_diff.calculate_differencesturns structured per-pixel timestamps into per-window differences for pixel pairs.delta_tdrives those two over a folder of files, pools the differences and saves them to a.feathertable, then histograms, fits and plots the coincidence peak.
Timing model¶
Each acquisition window is ~9 µs, but the ring-oscillator TDC measures only
within one period of the reference clock. The recorded time_series value is
the oscillator code on that range, and the timestamp is
on average. The ring sets that code width, so it does not change with the clock — only the number of codes does, as 1/f:
| clock | full scale | codes |
|---|---|---|
| 50 MHz | 20 ns | ~279 |
| 10 MHz | 100 ns | ~1397 |
| 5 MHz | 200 ns | ~2794 |
| 2 MHz | 500 ns | ~6985 |
That full scale is what every entry point needs, as open_shutter_time_s (one
clock period) or full_scale_s. It bounds a difference, so it is also the
accidental triangle's half-base — see
the triangle background.
The exact per-code bin widths come from a density-test LUT — pass it as
time_lut once you have one; see TDC calibration. A LUT
is specific to its clock; the loaders check that for you and refuse a
mismatch, because the silent version of that mistake costs 96 % of the
coincidences at 2 MHz.
Units¶
Every time parameter in the package is in seconds, with an _s suffix:
bin_width_s, plot_window_s, fit_window_s, support_s, time_unit_s,
delta_window_s, open_shutter_time_s. Histogram bin centres, edges and plot
axes are in picoseconds — that is the scale the TDC lives on — and the
conversion happens on entry.
There is one timestamp per photon, per window, per macropixel.
Fitting the peak: the background is a triangle¶
This is the part that most affects the number you report.
The accidental-coincidence background under an ORT delta-t peak is triangular, not flat, because the free-running oscillator has no cycle counter. Fitting a Gaussian on a flat background over a wide window therefore inflates the fitted sigma — you measure the background's slope as if it were jitter.
Two models are provided:
| model | fit | when to use |
|---|---|---|
gaussian / fit_gaussian_peak |
Gaussian on flat background | valid only on a narrow window around the peak |
gaussian_plus_triangle / fit_gaussian_on_triangle |
Gaussian on triangular background | the correct model for ORT delta-t over a wide window |
The full derivation, with the demonstration that the triangle is intrinsic rather than an artefact, is in the triangular background note.
Before you analyse: check the data¶
Run data_quality.plot_time_code_histogram
on a pixel or two first. When the TDC never stops, unpack still returns a full
array of plausible integers and this whole pipeline still produces a
peak-shaped histogram — it is simply meaningless. See
Data quality.
Large runs: the differences are streamed, not accumulated¶
calculate_and_save_timestamp_differences writes as it goes. Whenever the
buffered differences would exceed the 64 MB part cap they are flushed to a part
file, and the parts are concatenated into the final feather one Arrow record
batch at a time:
processed/
<dataset>_delta_t_parts/
<dataset>_delta_t_0.feather <- flushed during the run
<dataset>_delta_t_1.feather
...
<dataset>_delta_t.feather <- the combined result
This matters at scale. Holding a whole 10 000-file run in memory and writing it once fails twice over: the buffers exhaust RAM, and the single write then asks pyarrow for a multi-GB contiguous allocation on top of what is already held. Streaming makes the loop's memory ceiling one part plus one unpacked file, whatever the file count.
Two consequences worth knowing:
- A crashed run is recoverable. Every part flushed before the crash is a
complete, readable feather. Assemble them with
combine_delta_t_parts, or plot a single part directly viafeather_path. - The parts are always kept. They cost disk rather than memory, they are the crash insurance, and — see below — the read side streams them just as happily as the combined file, so there is nothing to gain from deleting them automatically. Remove the folder by hand once you have checked the combined feather.
Why 64 MB and not 10: unpack already holds a (32, 32, nframes) float64
array — about 82 MB at 10 000 frames — so below that the part buffer is not the
binding constraint, and a smaller cap only multiplies the number of part files.
The cap is the module constant _PART_MB, not a parameter: sharding is a
memory strategy and cannot change the numbers, so no call site needs to tune
it.
Reading it back: the histogram is streamed, not loaded¶
The write side streams, and so must the read side. A 10 000-file run's combined feather reaches ~135 GB; loading it into a DataFrame and pooling the columns needs it resident several times over, which simply does not happen.
compute_and_save_delta_histogram
reads it one Arrow record batch at a time instead. A histogram over fixed edges
is additive, so the counts can be summed batch by batch with one batch
resident — and the result is bin-for-bin what histogramming the whole pooled
array at once would have given. This is the exact baseline: no sampling, no
re-binning, no rounding.
Nothing downstream ever needed the raw differences. Both fit models take
(bin_centers, counts), and so does the plot; the pooled array was a pure
intermediate. So the counts are saved as a stage-1 artifact:
processed/
<dataset>_delta_t.feather <- ~135 GB, read once
<dataset>_delta_hist.npy <- the counts, a few MB
<dataset>_delta_hist.meta.json <- binning, source fingerprint, statistics
The sidecar records bin_width_s, plot_window_s, time_unit_s, pairs
and a fingerprint of the source files. A saved histogram is reused only when
all of it matches, so changing the binning or rewriting the feather re-streams
automatically; pass reuse=False (or reuse_histogram=False to the plot
driver) to force the full pass. Re-fitting, switching between the flat and
triangular background, or redrawing therefore costs a small load rather than
another read of the feather.
collect_and_plot_timestamp_differences goes through this, and returns
(counts, centers, fit) — enough to re-fit by hand without touching disk:
counts, centers, fit = dt.collect_and_plot_timestamp_differences(
path,
background="triangle",
open_shutter_time_s=100e-9,
plot_window_s=100e-9,
)
narrow = np.abs(centers) < 1000
dt.fit_gaussian_peak(centers[narrow], counts[narrow])
The .meta.json also reports padding_fraction: the share of the cells read
that were padding rather than data. The parts are written wide and padded to
the longest pair column, so an unbalanced blob pair inflates the feather —
that number says how much of the 135 GB is real.
Plotting without the combined file¶
feather_path accepts a *_delta_t_parts folder as well as a .feather, and
a run whose combined feather is missing falls back to its parts on its own. The
parts are complete feathers, so the combine step is optional for plotting:
Parts or combined file is not a numerical choice — the histogram is a sum over record batches either way, and a sum does not care which file a batch came out of, so the counts are identical. It is only an I/O choice, and the parts win it: histogramming them skips the combine pass, which reads and rewrites every byte of the run to produce a second copy of it. Working from the parts is the reason they are always kept. Keep the combined feather when you want one portable file per run, or when the data set is small enough to read whole into a DataFrame.
The other stage 1: histogram straight off the .bin¶
Everything above keeps every difference and histograms it later. That is the baseline — nothing is thrown away — but the artifact grows with the run, and the binning is still chosen at plot time, so every change of mind costs another pass over hundreds of GB.
calculate_and_save_delta_counts
runs the same loop and bins the differences onto a fixed native grid as it
finds them, keeping only the counts. It writes no feather at all:
processed/
<dataset>_delta_counts.npy <- (n_pairs, n_cells) int64
<dataset>_delta_counts.meta.json <- grid, pair labels, totals, progress
The size is set by the grid, not by the data. A 256-pair run on the default grid is 106 MB whether it came from 100 files or 10 000 — measured against a 3.22 GB feather for the same run, and the same 106 MB for one that reaches 135 GB. That is what makes every pair plottable at once instead of whichever columns you could afford to stream.
Choosing the grid¶
Two arguments, grid_s (cell width) and support_s (half-range):
| data | default cell | why |
|---|---|---|
raw codes (apply_TDC_calibration=False) |
one code, ~71.6 ps | delta_code is an integer, so counts-per-code is a re-encoding, not a summary — exactly lossless |
calibrated (unit='ps') |
1/10 code, ~7.2 ps | the per-pixel LUT smears the code lattice; 7.2 ps is ~5× finer than the jitter |
support_s defaults to twice the TDC full scale — read off the LUT when
there is one, else from open_shutter_time_s. A difference cannot exceed the
full scale, so that covers the whole physical support at any clock; a fixed
200 ns was 10× too wide at 50 MHz and 2.5× too narrow at 2 MHz. Anything landing
outside is reported as n_outside rather than dropped silently; if that is not
~0, widen it.
Re-binning is free, and exact¶
plot_window_s becomes a slice and bin_width_s a reduce, both in memory:
dt.collect_and_plot_delta_counts(
path,
pairs=["6,10-21,22"],
bin_width_s=71e-12,
plot_window_s=10e-9,
background="triangle",
full_scale_s=500e-9, # or leave it to open_shutter_time_s
)
By default the binning is snapped onto the grid, and the snap is not cosmetic:
- the width goes to a whole number of cells, or bins hold 9 then 10 then 9 cells and a smooth distribution picks up an ~11% sawtooth;
- that number is made odd. Bins are centred on zero, so their edges sit at half-integer multiples of the width — odd puts those edges on cell boundaries, even puts them through cell centres, splitting a cell at every boundary. Measured on a real 3.22 GB run: exactly zero difference from the feather path at 9, 11 and 13 cells per bin, ~1% at 8, 10 and 12;
- the window goes to a whole number of bins, so zero is a bin centre.
So with snapping on, this is not an approximation of the feather histogram —
it is the same histogram. Pass snap_to_grid=False to take your numbers
literally, and you will be warned about the sawtooth you are asking for.
The one honest disagreement is the outermost bin at each end. The feather path
cuts data at abs(delta) <= plot_window_s but draws edges half a bin beyond
it, so its first and last bins can only ever be part filled; the counts path
fills them. That, not the method, is why fitted sigma differs by ~1%.
What you give up¶
No per-event data survives, so there is no re-calibrating with a different LUT,
no re-cutting delta_window, and nothing that needs individual differences —
time ordering, splitting by acquisition, higher-order correlations. It also
does not make stage 1 meaningfully faster: unpacking dominates either way. The
win is artifact size and replot latency. Keep the feather until you are sure
you want none of that back.
Crash insurance without a part folder¶
The grid is rewritten every flush_every files (default 50), so a run that
dies leaves a valid artifact covering everything up to its last checkpoint, and
the next call resumes from it. There is only ever one file, so two runs cannot
interleave — the failure mode combine_delta_t_parts exists to guard against
simply does not arise here.
The sidecar records files_done and total_in_grid. The array is moved into
place before the sidecar, so a crash between the two moves could pair a newer
array with an older sidecar; resuming on that would re-count files and inflate
the total. The resume path checks the array against total_in_grid and starts
over rather than continue from something inconsistent.
Measuring the background instead of fitting it¶
subtract_background=True histograms the frame-shifted pixel pairs alongside
the real ones, so the accidental triangle can be subtracted bin-for-bin rather
than fitted. Both stage-1 paths take it, and both give the same residual — see
Removing the accidental background by shifting frames.
# counts path: the lags cost one small grid plane each
dt.calculate_and_save_delta_counts(path, [signal, idler], nframes=10_000,
subtract_background=True)
counts, centers, fit = dt.collect_and_plot_delta_counts(
path, subtract_background=True, peak_window_s=2e-9)
# feather path: same call, but the lags cost one column set each - so ~9x the
# feather at the default eight lags. Use fewer, or use the counts path.
dt.calculate_and_save_timestamp_differences(path, [signal, idler], nframes=10_000,
subtract_background=True,
background_lags=(1, 2, 3, 4))
counts, centers, fit = dt.collect_and_plot_timestamp_differences(
path, subtract_background=True, peak_window_s=2e-9)
The feather's lag columns are named <pair>@lag<k>; lag 0 keeps the bare pair
name, and pooling "every pair column" never picks up the shifted ones.
Overwriting: the five-second countdown¶
Every rewrite=True in this module routes through
store.confirm_rewrite. On a fresh run it
does nothing at all. When something really is about to be destroyed it names
each target with its size and timestamp, then counts down five seconds before
proceeding:
!! rewrite=True - the following data WILL BE OVERWRITTEN:
!! .../processed/SPDC_delta_t.feather (1.4 GB, written 2026-07-30 02:11)
!! .../processed/SPDC_delta_t_parts/ (23 file(s), 1.4 GB)
!! Press Ctrl-C within 5 s to abort and copy it somewhere safe.
The pause is a plain time.sleep, so Ctrl-C raises straight out of the
analysis with the data on disk untouched. This is aimed at the common accident:
a rewrite=True left in the script from the previous acquisition, silently
eating an overnight run. Set store.REWRITE_DELAY_S = 0 in an unattended
pipeline that has its own guard.
Note the part folder is named in the warning as well — a rewrite clears the previous run's parts, otherwise they would be pooled into the new output.
Pooling runs¶
combine_delta_t_feathers pools the per-run delta-t feathers under a parent
folder (many acquisitions of the same measurement) into one combined .feather
in a combined sub-folder of processed/. It never folds a previous combined
output back into itself, and it streams batch-by-batch just as the part
combiner does, so pooling a hundred runs costs one batch of memory rather than
the whole pooled data set.
Part files are not picked up by the search: they end in _<i>.feather, which
the *_delta_t.feather glob does not match, so nothing is ever counted twice.
API¶
delta_t ¶
Coincidence / timing-jitter (delta-t) pipeline for the ORT program.
Drives 'unpack' and 'calc_diff' over a folder, pools the per-pixel-pair timestamp differences into a '.feather' table, then histograms, fits and plots the coincidence peak.
Both halves stream. Stage 1 flushes size-capped part files as it goes, and the read side histograms the feather one Arrow record batch at a time rather than loading it - a 10 000-file run's combined feather reaches ~135 GB, which no amount of RAM makes loadable. Since a histogram over fixed edges is additive, the streamed counts are bin-for-bin what loading the whole table would have given; they are saved beside the feather so re-fitting costs a small load.
NOTE on fitting: the ORT accidental background is TRIANGULAR, not flat, because
the free-running oscillator has no cycle counter. Use
'fit_gaussian_on_triangle' over a wide window; 'fit_gaussian_peak' (flat
background) is valid only on a narrow window around the peak, and a flat fit
over a wide one inflates the fitted sigma. Or do not model it at all:
subtract_background=True measures it from frame-shifted pairs and subtracts
it, leaving the peak on a zero baseline
('dapkel.functions.background_subtraction',
docs/guide/background_subtraction.md).
The triangle's half-base is the TDC full scale, which is one period of the
reference clock - 20 ns at 50 MHz, 100 ns at 10 MHz, 500 ns at 2 MHz - so it is
not a constant and neither is the number of TDC codes. Every entry point here
takes open_shutter_time_s (or full_scale_s) rather than assuming 100 ns,
and the calibration LUT is checked against it: feeding a 10 MHz LUT to a 2 MHz
run discards every code past 100 ns, which is 79 % of the singles and 96 % of the
coincidences, and the surviving peak still looks plausible.
UNITS: every time parameter in this module is in seconds and named with an
_s suffix, per 'dapkel.core.timing'. Histogram edges, bin centres and plot
axes are in picoseconds - that is the scale the TDC lives on - and the
conversion happens on entry with 'timing.to_ps'.
Check a fresh acquisition with 'dapkel.functions.data_quality' first.
See docs/guide/coincidences.md and docs/ort_triangle_background.md.
calculate_and_save_timestamp_differences ¶
calculate_and_save_timestamp_differences(path: str, pixels: Sequence[Sequence[Pixel]], rewrite: bool = False, *, nframes: int, open_shutter_time_s: float, tag: str = 'ORT', mode: str = 'all_pairs', delta_window_s: float | None = None, apply_TDC_calibration: bool = True, daughterboard_number: str | None = None, motherboard_number: str | None = None, spad: int | str = 'average', max_files: int | None = None, subtract_background: bool = False, background_lags: int | Sequence[int] = BACKGROUND_LAGS) -> str
Unpack ORT data, compute blob-vs-blob delta-t, and save to feather.
Writes path/processed/<name>_delta_t.feather with one column per
pixel pair (named "ra,ca-rb,cb"), padded to a common length; every
requested pair gets a column even if it saw no coincidences.
The differences are streamed to disk as they are found, not held until
the end: the running buffer is flushed to
processed/<name>_delta_t_parts/<name>_delta_t_<run>_<i>.feather
whenever it would exceed '_PART_MB', and the parts are concatenated
into the final feather one record batch at a time. Memory therefore stays
flat in the number of '.bin' files, and a run that dies at file 9 000 of
10 000 leaves every completed part on disk - recover them with
'combine_delta_t_parts'.
The parts are kept. They are complete feathers in their own right and every read-side entry point takes a part folder as readily as a single file ('compute_and_save_delta_histogram' streams either one a record batch at a time), so they are the cheaper of the two things on disk to work from as well as the crash insurance. Delete the folder by hand once the combined feather has been checked.
Each run tags its parts with its own <run> id and records them in a
manifest, and a run refuses to start while another run's parts are still
in the folder. Part numbering restarts at 0 every run, so without both of
those a re-run would overwrite a dead run's first parts, leave its later
ones, and the combine step would pool the two - counting some differences
twice and inflating the feather.
With apply_TDC_calibration=True (default) the board's density-test LUT
converts timestamps to picoseconds before differencing, saving
delta_ps; otherwise raw delta_code is saved. Which one it is goes
into the feather's schema metadata as delta_unit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Path to the folder with the ORT '.bin' data files. |
required |
pixels
|
Sequence[Sequence[tuple[int, int]]]
|
Two groups |
required |
rewrite
|
bool
|
Overwrite an existing '.feather' (and part folder) for this data set.
A guard against accidental overwrites: the default is False, which
raises instead. When True and something really is about to be
destroyed, 'store.confirm_rewrite' names it and counts down five
seconds first, so a stale |
False
|
nframes
|
int
|
Number of frames stored in each '.bin' file. |
required |
open_shutter_time_s
|
float
|
Open shutter time, i.e. one reference-clock period, in seconds (e.g.
|
required |
tag
|
str
|
Filename fragment selecting the files. The default is |
'ORT'
|
mode
|
str
|
|
'all_pairs'
|
delta_window_s
|
float | None
|
Keep only differences with |
None
|
apply_TDC_calibration
|
bool
|
Apply the density-test TDC calibration (per-pixel code -> ps LUT)
before differencing, so the saved |
True
|
daughterboard_number
|
str | None
|
Daughterboard id (e.g. |
None
|
motherboard_number
|
str | None
|
Motherboard id (e.g. |
None
|
spad
|
int | str
|
Which SPAD's LUT to use: |
'average'
|
max_files
|
int | None
|
Process at most this many files. The default is None (all files). |
None
|
subtract_background
|
bool
|
Also difference the frame-shifted pairs, so the accidental
background can be measured rather than fitted. The default is False,
which writes exactly the feather it always has. When True each pair
gains one column per lag, named |
False
|
background_lags
|
int | Sequence[int]
|
Frame offsets used for the accidental estimate when
|
BACKGROUND_LAGS
|
Returns:
| Type | Description |
|---|---|
str
|
Path to the saved '.feather' file. |
Raises:
| Type | Description |
|---|---|
FileExistsError
|
Raised when the '.feather' already exists and |
ValueError
|
Raised on a calibration request with no board, or a 0 / negative /
repeated entry in |
Source code in src/dapkel/functions/delta_t.py
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 | |
combine_delta_t_parts ¶
combine_delta_t_parts(parts_dir: str, out_path: str | None = None, *, rewrite: bool = False, run: str | None = None) -> str
Assemble one run's delta-t part files into its combined feather.
'calculate_and_save_timestamp_differences' does this itself at the end of a successful run. Call it by hand to recover a run that died partway through: every part flushed before the crash is complete and usable, so the differences up to the last flush are not lost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parts_dir
|
str
|
The |
required |
out_path
|
str | None
|
Where to write the combined '.feather'. The default is None, meaning
|
None
|
rewrite
|
bool
|
Overwrite an existing combined feather. The default is False, which raises instead. When True, 'store.confirm_rewrite' counts down first. |
False
|
run
|
str | None
|
Which run's parts to assemble, when the folder holds more than one. The default is None, which requires the folder to describe exactly one run and raises otherwise rather than pooling them. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
Path to the saved combined '.feather'. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
Raised when the folder holds no part files. |
FileExistsError
|
Raised when the output exists and |
ValueError
|
Raised when the folder holds several runs and |
Source code in src/dapkel/functions/delta_t.py
1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 | |
combine_delta_t_feathers ¶
combine_delta_t_feathers(path: str, *, out_name: str | None = None, rewrite: bool = False, feathers: Sequence[str] | None = None, combined_dirname: str = 'combined') -> str
Pool the delta-t feathers under a folder into one combined feather.
Finds every *_delta_t.feather beneath path (each written by
'calculate_and_save_timestamp_differences' for one acquisition), pools
them per pixel-pair column — concatenating the differences of matching
pairs and taking the union of pairs across runs — and writes a single
combined '.feather' into path/<combined_dirname>. The combined table
keeps the one-column-per-pair, padded layout and the delta_unit
metadata, so it plots exactly like a single-run feather (pass it to
'collect_and_plot_timestamp_differences' via feather_path).
The pooling streams a record batch at a time (see
'_combine_delta_feathers'), so combining a hundred runs costs one batch of
memory rather than the whole pooled data set. Per-run part folders are not
picked up: their files end in _<i>.feather, so the *_delta_t.feather
search skips them and nothing is counted twice.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Parent folder holding the per-run subfolders (e.g.
|
required |
out_name
|
str | None
|
Base name for the combined file ( |
None
|
rewrite
|
bool
|
Overwrite an existing combined feather. The default is False, which raises instead. When True, 'store.confirm_rewrite' counts down first. |
False
|
feathers
|
Sequence[str] | None
|
Explicit list of feather paths to combine, bypassing the recursive
search under |
None
|
combined_dirname
|
str
|
Name of the output subfolder created under |
'combined'
|
Returns:
| Type | Description |
|---|---|
str
|
Path to the saved combined '.feather'. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
Raised when no |
FileExistsError
|
Raised when the combined feather exists and |
ValueError
|
Raised when the inputs mix |
Source code in src/dapkel/functions/delta_t.py
1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 | |
compute_and_save_delta_histogram ¶
compute_and_save_delta_histogram(path: str | None = None, *, feather_path: str | None = None, time_unit_s: float = CODE_S, bin_width_s: float = CODE_S, plot_window_s: float = 3e-09, pairs: Sequence[str] | None = None, subtract_background: bool = False, reuse: bool = True, quiet: bool = False) -> tuple[ndarray, ndarray, dict]
Stream a delta-t feather down to a saved histogram.
This is the expensive pass - it reads every record batch of every source -
and it is the only thing standing between a hundreds-of-GB feather and a
plot. The counts are saved to processed/<name>_delta_hist.npy with a
'.meta.json' sidecar, so replotting, re-fitting or changing the background
model costs a ~5 MB load rather than another full read.
The saved histogram is lossless for everything downstream: both fit
models take (bin_centers, counts), and so does the plot. Binning at
bin_width_s=timing.CODE_S (one TDC code) is the native resolution of
the data, so nothing is thrown away at the default settings.
The sidecar records the binning and a fingerprint of the sources; a saved
histogram is only reused when all of it matches, so changing
bin_width_s, plot_window_s, time_unit_s or pairs - or
rewriting the feather - re-streams automatically.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | None
|
Path to the raw-data folder. Its |
None
|
feather_path
|
str | None
|
Explicit delta-t '.feather' to read, or a |
None
|
time_unit_s
|
float
|
Seconds per raw TDC code, applied to a |
CODE_S
|
bin_width_s
|
float
|
Histogram bin width in seconds. The default is one TDC code. |
CODE_S
|
plot_window_s
|
float
|
Histogram half-range in seconds around zero. The default is |
3e-09
|
pairs
|
Sequence[str] | None
|
Restrict pooling to these pixel-pair column names. The default is None (pool every pair column). Frame-shifted columns are never pooled into the signal histogram. |
None
|
subtract_background
|
bool
|
Histogram every frame lag separately instead of pooling one signal
histogram, giving an |
False
|
reuse
|
bool
|
Load a matching saved histogram instead of re-streaming. The default is True. Pass False to force the full pass. |
True
|
quiet
|
bool
|
Suppress the progress bar and the summary. The default is False. |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
counts |
ndarray
|
Counts per bin, or |
edges |
ndarray
|
The bin edges the counts belong to. |
info |
dict
|
The stats from '_stream_delta_histogram' plus |
Raises:
| Type | Description |
|---|---|
ValueError
|
Raised when |
Source code in src/dapkel/functions/delta_t.py
1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 | |
calculate_and_save_delta_counts ¶
calculate_and_save_delta_counts(path: str, pixels: Sequence[Sequence[Pixel]], rewrite: bool = False, *, nframes: int, open_shutter_time_s: float, tag: str = 'ORT', mode: str = 'all_pairs', delta_window_s: float | None = None, apply_TDC_calibration: bool = True, daughterboard_number: str | None = None, motherboard_number: str | None = None, spad: int | str = 'average', max_files: int | None = None, grid_s: float | None = None, support_s: float | None = None, subtract_background: bool = False, background_lags: int | Sequence[int] = BACKGROUND_LAGS, flush_every: int = _FLUSH_EVERY, resume: bool = True) -> str
Unpack ORT data and histogram delta-t directly - no feather.
The alternative to 'calculate_and_save_timestamp_differences'. Same loop
(unpack -> per-pixel timestamps -> 'calc_diff.calculate_differences'), but
the differences are binned onto a fixed native grid as they are found and
only the counts are kept. Writes
processed/<name>_delta_counts.npy: one row per pixel pair, one column
per grid cell - and, with subtract_background, one plane per frame
lag on top of that.
The size is set by the grid, not by the data - a 10 000-file run and a
100-file run produce the same ~100 MB artifact, against 3-135 GB of
feather. That is what makes every pair plottable at once, and it makes
re-binning free: 'rebin_delta_counts' turns the grid into any coarser
histogram in memory, so bin_width_s and plot_window_s stop being
compute-time decisions.
What it gives up. No per-event data survives, so there is no
re-calibrating with a different LUT, no re-cutting delta_window_s, and
no analysis that needs individual differences (time ordering, splitting by
acquisition, higher-order correlations). Keep the feather until you have
decided you never want those.
How exact it is. For raw codes (apply_TDC_calibration=False) the
default grid is one code and the result is lossless - integers binned
onto their own lattice. For calibrated data the LUT smears the lattice, so
each difference is displaced by up to grid_s / 2 (~3.6 ps at the
default), which is far below the detector jitter but is not bit-exact
against the feather.
Crash insurance is one file rather than a part folder: the grid is
rewritten every flush_every files, so a run that dies leaves a valid
artifact covering everything up to its last checkpoint, and resume
picks it up. Two runs cannot interleave, because there is only ever one
file to write.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Path to the folder with the ORT '.bin' data files. |
required |
pixels
|
Sequence[Sequence[tuple[int, int]]]
|
Two groups |
required |
rewrite
|
bool
|
Overwrite a complete counts artifact for this data set. The default
is False, which raises instead. An incomplete one is resumed rather
than overwritten (see |
False
|
nframes
|
int
|
Number of frames stored in each '.bin' file. Part of the resume signature - it sets how much of each file is read, so a checkpoint only resumes against the same value. |
required |
open_shutter_time_s
|
float
|
Open shutter time, i.e. one reference-clock period, in seconds (e.g.
|
required |
tag
|
str
|
Filename fragment selecting the files. The default is |
'ORT'
|
mode
|
str
|
|
'all_pairs'
|
delta_window_s
|
float | None
|
Keep only differences with |
None
|
apply_TDC_calibration
|
bool
|
Apply the per-pixel code -> ps LUT before differencing. The default is
True, which requires |
True
|
daughterboard_number
|
str | None
|
Daughterboard id (e.g. |
None
|
motherboard_number
|
str | None
|
Motherboard id (e.g. |
None
|
spad
|
int | str
|
Which SPAD's LUT to use. The default is |
'average'
|
max_files
|
int | None
|
Process at most this many files. The default is None (all files). |
None
|
grid_s
|
float | None
|
Width of one grid cell, in seconds. The default is None, meaning one TDC code ('core.timing.CODE_S', ~71.6 ps) for raw-code data - where that is exact - and one tenth of a code (~7.2 ps) for calibrated data. |
None
|
support_s
|
float | None
|
Half-range of the grid, in seconds. The default is None, which is twice
the TDC full scale - read off the LUT when there is one, else from
|
None
|
subtract_background
|
bool
|
Also histogram the differences between frame-shifted pixel pairs, so
the accidental background can be measured rather than fitted. The
default is False, which writes exactly the 2D artifact it always has.
When True the artifact gains a leading lag axis with lag 0 - the
ordinary within-frame histogram, bin-for-bin identical to the 2D one -
as plane 0; see 'dapkel.functions.background_subtraction' and
|
False
|
background_lags
|
int | Sequence[int]
|
Frame offsets used for the accidental estimate when
|
BACKGROUND_LAGS
|
flush_every
|
int
|
Files between checkpoint writes. The default is 50. |
_FLUSH_EVERY
|
resume
|
bool
|
Continue an incomplete artifact for the same inputs instead of starting over. The default is True. |
True
|
Returns:
| Type | Description |
|---|---|
str
|
Path to the saved '.npy'. |
Raises:
| Type | Description |
|---|---|
FileExistsError
|
Raised when a complete artifact exists and |
ValueError
|
Raised on a bad |
Source code in src/dapkel/functions/delta_t.py
2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 | |
load_delta_counts ¶
load_delta_counts(path: str | None = None, *, counts_path: str | None = None, pairs: Sequence[str] | None = None, pool: bool = True, time_unit_s: float = CODE_S) -> tuple[ndarray, ndarray, dict]
Load a saved counts grid and its cell centres.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | None
|
Data folder whose |
None
|
counts_path
|
str | None
|
Explicit '.npy' to load, ignoring |
None
|
pairs
|
Sequence[str] | None
|
Restrict to these pixel-pair labels. The default is None (all). |
None
|
pool
|
bool
|
Sum the selected pairs into one histogram. The default is True; pass False to keep the pair axis. |
True
|
time_unit_s
|
float
|
Seconds per raw TDC code, used to place the cell centres of an
uncalibrated ( |
CODE_S
|
Returns:
| Name | Type | Description |
|---|---|---|
counts |
ndarray
|
Pooled counts, or per-pair counts when |
centers |
ndarray
|
Grid cell centres, in picoseconds whatever the stored unit. |
info |
dict
|
The sidecar, plus |
Raises:
| Type | Description |
|---|---|
ValueError
|
Raised when neither |
FileNotFoundError
|
Raised when the artifact does not exist. |
Source code in src/dapkel/functions/delta_t.py
2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 | |
rebin_delta_counts ¶
rebin_delta_counts(counts: ndarray, centers: ndarray, *, bin_width_s: float, plot_window_s: float) -> tuple[ndarray, ndarray]
Reduce a native counts grid onto a coarser histogram, in memory.
Each grid cell is assigned whole to the target bin its centre falls in.
That is exact whenever bin_width_s is a whole multiple of the grid
cell and the two lattices line up; otherwise a cell straddling a target
edge goes entirely one way, which is the rebinning, not the data. Bin on a
multiple of the grid when comparing against the feather path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
counts
|
ndarray
|
1D pooled counts on the native grid, from 'load_delta_counts'. |
required |
centers
|
ndarray
|
The grid cell centres in ps, from 'load_delta_counts'. |
required |
bin_width_s
|
float
|
Target bin width, in seconds. |
required |
plot_window_s
|
float
|
Target half-range, in seconds. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
counts |
ndarray
|
Counts per target bin. |
edges |
ndarray
|
The target bin edges in ps - the same '_delta_hist_edges' the feather path uses, so the two histograms are directly comparable. |
Source code in src/dapkel/functions/delta_t.py
gaussian ¶
Gaussian peak on a flat background.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input positions. |
required |
amp
|
float
|
Peak amplitude above the background. |
required |
mu
|
float
|
Peak centre. |
required |
sigma
|
float
|
Standard deviation of the Gaussian. |
required |
bkg
|
float
|
Flat background level. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Model values |
Source code in src/dapkel/functions/delta_t.py
gaussian_plus_triangle ¶
gaussian_plus_triangle(x: ndarray, amp: float, mu: float, sigma: float, bkg: float, half_base: float) -> ndarray
Gaussian peak on a symmetric triangular background.
The correct accidental model for ORT: uncorrelated free-running phases
difference into a triangle (zero at +/- half_base), with correlated
pairs adding a Gaussian at mu ~ 0. Why:
docs/ort_triangle_background.md.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input positions (timestamp differences, same units as |
required |
amp
|
float
|
Gaussian amplitude above the triangle. |
required |
mu
|
float
|
Gaussian centre. |
required |
sigma
|
float
|
Gaussian standard deviation. |
required |
bkg
|
float
|
Triangle height at its apex ( |
required |
half_base
|
float
|
Half-width of the triangle base; the background is zero for
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Model values |
Source code in src/dapkel/functions/delta_t.py
two_gaussians_on_triangle ¶
two_gaussians_on_triangle(x: ndarray, amp_n: float, mu: float, sigma_n: float, amp_b: float, dmu: float, sigma_ratio: float, bkg: float, half_base: float) -> ndarray
Narrow + broad Gaussian on a triangular background.
A single Gaussian fitted to the ORT coincidence peak reports the area-weighted mixture of two physically distinct timing populations, which is why it lands near 500-700 ps while the designer's focused-laser number is tens of ps:
- narrow - photons absorbed inside the SPAD depletion region, timed by the avalanche build-up alone (tens of ps, at or below this TDC's ~77 ps LSB, so it is resolution-limited here);
- broad - photons absorbed in the field-free epi/substrate below the junction, which reach the multiplication region by diffusion. The true shape of that component is one-sided exponential, not Gaussian; a Gaussian is the cheap two-parameter stand-in that separates the scales without committing to the tail shape. If the residuals stay structured in the flanks, that is the signal to move to an exponentially modified Gaussian instead.
The broad width is parameterised as a ratio rather than an absolute
sigma so the two components cannot swap roles mid-fit: sigma_ratio >= 1
makes sigma_n the narrow one by construction. sigma_ratio = 1
collapses the model to a single Gaussian, so a data set that does not need
two components degenerates gracefully instead of fitting noise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Input positions (timestamp differences, same units as |
required |
amp_n
|
float
|
Amplitude of the narrow component above the triangle. |
required |
mu
|
float
|
Centre of the narrow component. |
required |
sigma_n
|
float
|
Standard deviation of the narrow component. |
required |
amp_b
|
float
|
Amplitude of the broad component above the triangle. |
required |
dmu
|
float
|
Centre of the broad component relative to |
required |
sigma_ratio
|
float
|
|
required |
bkg
|
float
|
Triangle height at its apex ( |
required |
half_base
|
float
|
Half-width of the triangle base; the background is zero for
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Model values: narrow Gaussian + broad Gaussian + |
Source code in src/dapkel/functions/delta_t.py
2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 | |
fit_gaussian_on_triangle ¶
fit_gaussian_on_triangle(bin_centers: ndarray, counts: ndarray, half_base: float, fit_half_base: bool = True) -> dict
Fit a Gaussian peak on a triangular background (Kelpie ORT model).
Use this rather than 'fit_gaussian_peak' for ORT delta-t over a wide window: the background is triangular, and a flat-background fit lets the peak swallow the triangle and reports an inflated sigma.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bin_centers
|
ndarray
|
Histogram bin centres (should span roughly the full +/- half_base). |
required |
counts
|
ndarray
|
Histogram counts. |
required |
half_base
|
float
|
Initial triangle half-base (~one oscillator period; ~100 ns in ps, or
~1300 in code units). Refined when |
required |
fit_half_base
|
bool
|
Let |
True
|
Returns:
| Type | Description |
|---|---|
dict
|
Fit results with keys |
Raises:
| Type | Description |
|---|---|
ValueError
|
Raised when |
Source code in src/dapkel/functions/delta_t.py
2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 | |
fit_two_gaussians_on_triangle ¶
fit_two_gaussians_on_triangle(bin_centers: ndarray, counts: ndarray, half_base: float, fit_half_base: bool = True, poisson_weights: bool = True) -> dict
Fit a narrow + broad Gaussian on a triangular background.
Separates the two timing populations a single Gaussian averages together (see 'two_gaussians_on_triangle'), so the fast avalanche core can be compared against a focused-laser jitter number and the slow diffusion component quantified instead of inflating one sigma.
Seeded from 'fit_gaussian_on_triangle': its sigma is the area-weighted mixture of both components, which makes it a sound seed for the broad one (it carries most of the area) and the narrow component is then seeded from what is left over in the central few bins. Fitting eight free parameters from cold seeds does not converge reliably; from these it does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bin_centers
|
ndarray
|
Histogram bin centres. Should span well past the peak so the triangle is constrained - a window of a few ns cannot separate the components. |
required |
counts
|
ndarray
|
Histogram counts. |
required |
half_base
|
float
|
Initial triangle half-base (~one oscillator period; ~100 ns in ps, or
~1300 in code units). Refined when |
required |
fit_half_base
|
bool
|
Let |
True
|
poisson_weights
|
bool
|
Weight each bin by |
True
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
Raised when |
Source code in src/dapkel/functions/delta_t.py
3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 | |
fit_gaussian_peak ¶
Fit a Gaussian-plus-background peak with scipy.curve_fit.
The flat background is seeded from the histogram tails and the peak
centre/width from the background-subtracted moments, then all four
parameters (amp, mu, sigma, bkg) are refined by a
least-squares fit of 'gaussian'.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bin_centers
|
ndarray
|
Histogram bin centres. |
required |
counts
|
ndarray
|
Histogram counts. |
required |
fit_window
|
float | None
|
If given, restrict the fit to |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
Fit results with keys |
Source code in src/dapkel/functions/delta_t.py
3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 | |
collect_and_plot_timestamp_differences ¶
collect_and_plot_timestamp_differences(path: str | None = None, *, open_shutter_time_s: float | None = None, time_unit_s: float = CODE_S, bin_width_s: float = CODE_S, plot_window_s: float = 3e-09, fit_window_s: float | None = 1e-09, background: str = 'flat', full_scale_s: float | None = None, feather_path: str | None = None, pairs: Sequence[str] | None = None, label: str = 'spdc', cmap_title: str | None = None, reuse_histogram: bool = True, subtract_background: bool = False, peak_window_s: float = 1e-09, wide_rebin: int = 25, support_s: float | None = None) -> tuple[ndarray, ndarray, dict]
Histogram the saved delta-t feather, fit the peak, and plot it.
Pools the selected pair columns of a saved feather, converts to
picoseconds (per the delta_unit metadata), histograms, fits the
coincidence peak and saves the figure. Old single-column feathers
(delta_code / delta_ps) are still read.
The feather is streamed through 'compute_and_save_delta_histogram',
one Arrow record batch at a time, and never held in memory - a full run's
combined feather is far too large to load. The counts are identical to
histogramming the whole pooled array at once; only the memory profile
differs. The histogram is cached in processed/, so re-fitting or
changing the background model does not re-read the feather.
Locate the feather either from path (its processed/) or directly
via feather_path, which ignores path entirely - that is how a
combined feather is plotted. Either may point at a *_delta_t_parts
folder to plot straight from the part files.
With subtract_background the histogram that gets fitted is the lag-0
minus shifted-frames residual: the accidental pedestal is measured from
frame-shifted pairs and subtracted, rather than modelled. All the lags are
histogrammed in the same single streaming pass. Needs a feather written
with calculate_and_save_timestamp_differences(...,
subtract_background=True).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | None
|
Path to the raw-data folder (its |
None
|
open_shutter_time_s
|
float | None
|
Open shutter time, i.e. one reference-clock period, in seconds. Used as
the fallback for the TDC full scale, hence for the triangle half-base
and the default streaming support. The default is None, which is fine
only for |
None
|
time_unit_s
|
float
|
Seconds per raw TDC code, applied to a |
CODE_S
|
bin_width_s
|
float
|
Histogram bin width in seconds. The default is one TDC code. |
CODE_S
|
plot_window_s
|
float
|
Histogram/plot half-range in seconds around zero. The default is
|
3e-09
|
fit_window_s
|
float | None
|
Fit half-range in seconds around the tallest bin. Only used for
|
1e-09
|
background
|
str
|
Peak-and-background model. |
'flat'
|
full_scale_s
|
float | None
|
TDC full scale in seconds, which is the triangle half-base: the
accidental background peaks at 0 and reaches zero at
|
None
|
feather_path
|
str | None
|
Explicit feather to read and plot directly, or a |
None
|
pairs
|
Sequence[str] | None
|
Restrict pooling to these pixel-pair column names (e.g.
|
None
|
label
|
str
|
Short label for the title and output file ( |
'spdc'
|
cmap_title
|
str | None
|
Optional override for the plot title. The default is None. |
None
|
reuse_histogram
|
bool
|
Reuse a saved histogram whose binning and sources match, instead of re-streaming the feather. The default is True. Pass False to force the full pass. |
True
|
subtract_background
|
bool
|
Subtract the frame-shifted accidentals before fitting, and write the
three-panel diagnostic beside the fit. The default is False. Requires a
feather written with |
False
|
peak_window_s
|
float
|
Half-range the model-free coincidence count is summed over, in seconds.
|
1e-09
|
wide_rebin
|
int
|
Histogram bins per bin of the diagnostic's two full-support panels.
|
25
|
support_s
|
float | None
|
Half-range the lag histograms are streamed over, in seconds.
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
counts |
ndarray
|
Counts per bin - the histogram that was fitted and plotted, or the
residual when |
centers |
ndarray
|
Bin centres in picoseconds, to re-fit without re-reading the feather. |
fit |
dict
|
The fit results plus |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
Raised when the delta-t '.feather' cannot be found. |
ValueError
|
Raised when |
Source code in src/dapkel/functions/delta_t.py
3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 | |
collect_and_plot_delta_counts ¶
collect_and_plot_delta_counts(path: str | None = None, *, counts_path: str | None = None, open_shutter_time_s: float | None = None, time_unit_s: float = CODE_S, bin_width_s: float = CODE_S, plot_window_s: float = 3e-09, fit_window_s: float | None = 1e-09, background: str = 'flat', full_scale_s: float | None = None, pairs: Sequence[str] | None = None, label: str = 'spdc', cmap_title: str | None = None, snap_to_grid: bool = True, subtract_background: bool = False, peak_window_s: float = 1e-09, wide_rebin: int = 25) -> tuple[ndarray, ndarray, dict]
Rebin a saved counts grid, fit the peak, and plot it.
The counterpart of 'collect_and_plot_timestamp_differences' for the feather-free path, and deliberately the same function underneath: only the histogram differs, so the two can be compared directly.
With subtract_background the histogram that gets fitted is the lag-0
minus shifted-frames residual instead of the raw one - same fit, same
figure, on a distribution whose accidental pedestal has been measured away
rather than modelled. Needs an artifact written with
calculate_and_save_delta_counts(..., subtract_background=True).
Nothing is read from disk except one ~100 MB array, and the rebinning is
in memory, so changing bin_width_s or plot_window_s costs
milliseconds rather than a full pass over the data - and every pixel pair
is available at once instead of whichever columns were streamed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | None
|
Data folder whose |
None
|
counts_path
|
str | None
|
Explicit counts '.npy', ignoring |
None
|
open_shutter_time_s
|
float | None
|
Open shutter time, i.e. one reference-clock period, in seconds. The
fallback source for the TDC full scale, hence the triangle half-base.
The default is None, which is fine only for |
None
|
time_unit_s
|
float
|
Seconds per raw TDC code, for an uncalibrated grid. The default is 'core.timing.CODE_S' (~71.6 ps), which is clock-independent. |
CODE_S
|
bin_width_s
|
float
|
Histogram bin width in seconds. The default is one TDC code. Use a whole multiple of the artifact's grid cell for an exact rebin. |
CODE_S
|
plot_window_s
|
float
|
Histogram/plot half-range in seconds around zero. The default is
|
3e-09
|
fit_window_s
|
float | None
|
Fit half-range in seconds around the tallest bin. |
1e-09
|
background
|
str
|
|
'flat'
|
full_scale_s
|
float | None
|
Triangle half-base seed in seconds - the TDC full scale. The default is
None, which resolves it from the artifact's own
|
None
|
pairs
|
Sequence[str] | None
|
Restrict pooling to these pixel-pair labels. The default is None (pool every pair). |
None
|
label
|
str
|
Short label for the title and output file. The default is |
'spdc'
|
cmap_title
|
str | None
|
Optional override for the plot title. The default is None. |
None
|
snap_to_grid
|
bool
|
Round |
True
|
subtract_background
|
bool
|
Subtract the shifted-frame accidentals before fitting. The default is
False. Requires a lagged artifact, and only makes sense with
|
False
|
peak_window_s
|
float
|
Half-range the model-free coincidence count is summed over, in seconds.
|
1e-09
|
wide_rebin
|
int
|
Grid cells per bin in the two full-support panels of the diagnostic
figure. |
25
|
Returns:
| Name | Type | Description |
|---|---|---|
counts |
ndarray
|
Counts per bin - the rebinned histogram that was fitted and plotted,
or the residual when |
centers |
ndarray
|
Bin centres in picoseconds. |
fit |
dict
|
As 'collect_and_plot_timestamp_differences'. With
|
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
Raised when the counts artifact cannot be found. |
ValueError
|
Raised when |
Notes
Re-binning already-binned data onto an incommensurate width is not a rounding detail, it is a ripple. A 71 ps bin over a 7.7 ps grid holds 9 or 10 cells depending on where it lands, so a perfectly smooth distribution comes out with an ~11% bin-to-bin sawtooth - on real data that moved the peak bin by 8%.
With snap_to_grid the rebin is not merely close to the feather path's
histogram, it is identical to it, because the target bins then hold a
whole odd number of cells and their edges fall between cells rather than
through them. Verified bin-for-bin on a 3.22 GB run: zero difference at 9,
11 and 13 cells per bin, ~1% at 8, 10 and 12. See '_snap_binning'.
The one place the two paths still disagree is the outermost bin at each
end, where the feather path cuts the data at abs(delta) <= window but
draws edges half a bin beyond it, so those bins can only ever be part
filled. Here they are filled properly.
Source code in src/dapkel/functions/delta_t.py
3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 | |
calc_diff ¶
Per-window timestamp differences for pairs of pixels.
The low-level, daplis-style counterpart of daplis.functions.calc_diff: it
takes an already-structured mapping of per-pixel, per-frame timestamps and
returns the differences for the requested pixel pairs. Unpacking, structuring
and saving are handled one level up in 'dapkel.functions.delta_t'.
A coincidence is a within-window event: in a frame where both pixels of a pair hold a valid TDC code, the difference of their codes is one entry of the delta-t distribution. Correlated pairs (e.g. SPDC signal/idler) pile up at delta = 0.
See docs/guide/coincidences.md.
as_pixel_list ¶
Normalise a detector spec into a list of (row, col) pixels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group
|
tuple[int, int] | Sequence[tuple[int, int]]
|
A single |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
List of |
Source code in src/dapkel/functions/calc_diff.py
calculate_differences ¶
calculate_differences(pixel_timestamps: dict[Pixel, ndarray], pixels: Sequence[Sequence[Pixel]], delta_window_native: float | None = None, mode: str = 'all_pairs') -> dict[str, ndarray]
Compute per-window timestamp differences for pairs of pixels.
For each requested pixel pair (a, b) the difference t_b - t_a
is taken in every acquisition window (frame) where both pixels hold a
valid timestamp. The per-pixel arrays must be aligned by frame index
(same length, NaN where the pixel had no photon that frame), which is
how 'delta_t' structures them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pixel_timestamps
|
dict[tuple[int, int], ndarray]
|
Mapping |
required |
pixels
|
Sequence[Sequence[tuple[int, int]]]
|
Two groups |
required |
delta_window_native
|
float | None
|
If given, keep only differences with
|
None
|
mode
|
str
|
|
'all_pairs'
|
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
Mapping |
Raises:
| Type | Description |
|---|---|
ValueError
|
Raised when |