Hitmaps and photon rate¶
A hitmap is the per-pixel occupancy of the sensor — how often each macropixel
registered a photon over the acquisition. How that is measured depends on which
Kelpie program wrote the data, so there are two modes, matched to what
unpack actually returns.
The two modes¶
mode="timestamp" — the ORT program¶
Every frame, each macropixel reports the timestamp of the first photon it saw (from one of its four micropixels); the later photons are not read out. This is a single first-arrival time per macropixel. It is not an array-wide "who fired first" ranking, and one macropixel's timestamp is not comparable to another's.
The hitmap therefore counts, per pixel, the number of frames carrying a valid
timestamp — i.e. how often that macropixel fired. Validity is time_series > 0:
unpack leaves non-fired slots at <= 0, so the threshold follows from the
decoding and is not a tunable.
photon_counts is not a photon count in ORT
In this mode unpack's photon_counts is not a count — those bits are
the low bits of the coarse timestamp (see unpack). Validity
must come from time_series, never from photon_counts.
mode="count" — the S*C / ORC programs¶
No timestamp is recorded; the data holds per-pixel photon counts, and the hitmap sums those over every frame. This is the same quantity DCR accumulates before dividing by the acquisition time.
Photon rate¶
The raw map is counts (or occupancy). The rate is that divided by the wall-clock duration of the acquisition, the same way in both modes:
in Hz — photons per second of experiment. Every frame is one acquisition cycle,
so nframes * n_files * cycle is the elapsed time of the run.
The cycle depends on the firmware¶
Two versions, selected with firmware_version
(core.timing.resolve_cycle_time):
firmware_version |
cycle | notes |
|---|---|---|
"short_exposure" (default) |
always 9 µs | the photon-sensitive exposure inside the cycle is 50–500 ns, typically 100 ns |
"long_exposure" |
exp_time_s + 9 µs |
you set X, the firmware adds 9 µs. exp_time_s is required |
"short_window" / "full_window" are accepted as aliases of the two, since
that is what resolve_live_time and
DCR call them.
frame_rate_cnt.txt is not used, on purpose
The acquisition exe writes a frame_rate_cnt.txt beside the data. Nothing
in dapkel reads it: it is produced by an exe we do not control, so no
analysis result is allowed to depend on it. The cycle length is a property
of the firmware and the requested exposure, and Kelpie_run.m states it
directly: exp_time_s = 0 → 9 µs, 1e-6 → 10 µs, 2e-6 → 11 µs.
It is worth knowing what the counter says, though, because it disagrees with the nominal figure. It is a 200 MHz tick count over the whole run, latched by the firmware after the acquisition reports done, and across every sample folder on disk it fits
exactly — the 138 (690 ns) a fixed per-run overhead, the exposure window
appearing once per run rather than per frame, and N the frame period. N
comes out at 1937–1954 ticks = 9.685–9.770 µs, so the real cycle is
685–770 ns per frame longer than the nominal 9 µs, and a rate normalised by
9 µs runs ~7.8 % high. N does not track the window (a 50 → 500 ns
window moves it by 14 ticks, not 90), which is the direct evidence that the
window sits inside the cycle rather than extending it. What the extra
~700 ns per frame is has not been established.
What the number means¶
Under timestamp mode only the first photon of a cycle is timestamped, so
the map saturates at one firing per cycle — 111 kHz at a 9 µs cycle — and
under-reports as it approaches that. It is a firing rate, not a corrected
photon flux.
exposure_window is recorded and reported as a duty cycle but does not
enter the rate. Dividing by it instead would give the rate during the
exposure, which is this figure times cycle / exposure — 90× for 9 µs / 100 ns.
Both are defensible quantities; this one is per second of experiment, which is
what the printed line and the colorbar say.
nframes is yours to supply, not the file's¶
The rate is proportional to 1 / nframes, and the frame count cannot be
trusted to the file. Every sample .bin is a whole power-of-two number of
frames — 32 MiB (16 384) or 16 MiB (8 192) — which is not what a 10 000-frame
acquisition should produce, so the relation between the number you enter and
the file you get is not established; see
io.frames_in_file.
Both this package's unpack and the reference kelpie_data_ddr3.m read only
the first nframes frames, so passing the number you acquired with gives
exactly the frames MATLAB gives. That is why nframes is a required argument
everywhere and is stored in the sidecar: it is the acquisition setting, and
the rate is only right if it matches what the camera actually recorded.
Compute once, plot many times¶
Analysis and plotting are decoupled. compute_and_save_hitmap writes the array
to processed/ with a .meta.json sidecar recording the frame count, the
firmware version and the resolved cycle; collect_and_plot_hitmap reads that
back and never re-unpacks the binary data. Retrying a colormap costs a load, not
a full re-read of the raw files. See
Adding an analysis for the stage rules.
from dapkel.functions import hitmap_analysis as hm
# short-exposure firmware: 9 µs cycle, 100 ns exposure inside it
hm.compute_and_save_hitmap(path, 10_000, "timestamp", "ORT",
exposure_window=100e-9)
# long-exposure firmware: you set X, the firmware adds 9 µs
hm.compute_and_save_hitmap(path, 10_000, "timestamp", "ORT",
firmware_version="long_exposure", exp_time_s=91e-6)
hm.collect_and_plot_hitmap(path, "timestamp", "ORT") # counts + rate figures
Because the sidecar stores the frame count and the cycle, a saved hitmap can still be turned into a rate months later without remembering the original call arguments. A hitmap saved before the sidecar carried them gets the counts figure and a note instead of a rate.
API¶
hitmap_analysis ¶
Per-pixel sensor occupancy (hitmaps), in counts and in photon rate.
Two modes, matched to what 'unpack' returns:
* ``mode="timestamp"`` (``ORT``) - counts frames carrying a valid
first-photon timestamp. At most one firing per macropixel per frame.
* ``mode="count"`` (``S*C``/``ORC``) - sums the per-pixel photon counts.
Both are turned into a photon rate the same way: divide by the total live
(shutter-open) time of the acquisition,
nframes * n_files * open_shutter_time_s - not the full ~9 us frame period.
Only the open shutter window is photon-sensitive; normalising by the whole frame
would dilute the rate by the frame/window ratio instead of reporting the flux
actually seen during the exposure. See docs/guide/hitmap.md.
compute_hitmap ¶
compute_hitmap(folder: str, nframes: int, mode: str = 'timestamp', tag: str = 'ORT', max_files: int | None = None) -> ndarray
Unpack the binary data and compute the (32, 32) hitmap.
Builds a single-readout hitmap from the '.bin' files matching tag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
str
|
Path to the folder with the '.bin' data files. |
required |
nframes
|
int
|
Number of frames stored in each '.bin' file. |
required |
mode
|
str
|
|
'timestamp'
|
tag
|
str
|
Filename fragment selecting the files (e.g. |
'ORT'
|
max_files
|
int | None
|
Process at most this many files (after natural sorting). Useful for a quick preview over a large batch. The default is None (all files). |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The (32, 32) hitmap. |
Source code in src/dapkel/functions/hitmap_analysis.py
compute_hitmap_64 ¶
Unpack the four S*C tags and assemble the (64, 64) count hitmap.
Accumulates the four SPAD tags' photon counts (mode='count') and
interleaves them onto the full sensor. Only the count programs (S*C)
tile this way; ORT/ORC are (32, 32) and use 'compute_hitmap'.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
str
|
Path to the folder with the S*C '.bin' data files. |
required |
nframes
|
int
|
Number of frames stored in each '.bin' file. |
required |
max_files
|
int | None
|
Process at most this many files per tag. The default is None. |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The (64, 64) accumulated-count hitmap for the full sensor. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
Raised when no '.bin' files for any of the four SPAD tags are found in the folder. |
Source code in src/dapkel/functions/hitmap_analysis.py
plot_hitmap ¶
plot_hitmap(hitmap: ndarray, cmap: str | None = None, *, clabel: str = 'hits', name: str = 'Hitmap', show_total: bool = True) -> Figure
Render a (32, 32)/(64, 64) map as a 2D image.
Used both for the raw counts/occupancy map and for the derived rate map (see 'collect_and_plot_hitmap'); the caller sets the colorbar label, title name and whether a total is meaningful.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hitmap
|
ndarray
|
Map to plot, e.g. from 'compute_hitmap'/'compute_hitmap_64', or a rate map (counts divided by the active acquisition time). |
required |
cmap
|
str | None
|
Matplotlib colormap name. The default is None — use the active
style's |
None
|
clabel
|
str
|
Colorbar label / units (e.g. |
'hits'
|
name
|
str
|
Map name used in the title (e.g. |
'Hitmap'
|
show_total
|
bool
|
Include the summed total in the title. Meaningful for counts, not for a rate map. The default is True. |
True
|
Returns:
| Type | Description |
|---|---|
Figure
|
The generated figure. |
Source code in src/dapkel/functions/hitmap_analysis.py
compute_and_save_hitmap ¶
compute_and_save_hitmap(path: str, nframes: int, mode: str = 'timestamp', tag: str = 'ORT', *, firmware_version: str = 'short_window', exp_time_s: float | None = None, open_shutter_time_s: float | None = 5e-07, max_files: int | None = None) -> str
Unpack the '.bin' files and save the (32, 32) hitmap to '.npy'.
Unpacks once and saves processed/<name>_<tag>_hitmap.npy plus a
.meta.json sidecar recording the frame count and the live time, so
'collect_and_plot_hitmap' can later render the photon-rate map without
re-unpacking.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Path to the folder with the '.bin' data files. |
required |
nframes
|
int
|
Number of frames the acquisition was set to record per '.bin' file.
Also recorded in the sidecar: the photon rate is
|
required |
mode
|
str
|
|
'timestamp'
|
tag
|
str
|
Filename fragment selecting the files. The default is |
'ORT'
|
firmware_version
|
str
|
Which firmware took the data, i.e. how much of each ~9 µs frame is
photon-sensitive: |
'short_window'
|
exp_time_s
|
float | None
|
Legacy exposure time requested of |
None
|
open_shutter_time_s
|
float | None
|
Photon-sensitive shutter window per frame, in seconds (50-500 ns on
the short-window firmware). This is the rate's denominator - required
for |
5e-07
|
max_files
|
int | None
|
Process at most this many files. The default is None (all files). |
None
|
Returns:
| Type | Description |
|---|---|
str
|
Path to the saved '.npy' hitmap. |
Source code in src/dapkel/functions/hitmap_analysis.py
353 354 355 356 357 358 359 360 361 362 363 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 | |
compute_and_save_hitmap_64 ¶
compute_and_save_hitmap_64(path: str, nframes: int, *, firmware_version: str = 'short_window', exp_time_s: float | None = None, open_shutter_time_s: float | None = 5e-07, max_files: int | None = None) -> str
Assemble and save the (64, 64) full-sensor count hitmap to '.npy'.
Like 'compute_and_save_hitmap' but for the four-quadrant (64, 64)
accumulated-count map (see 'compute_hitmap_64'); saved as
<name>_64_hitmap.npy in processed, with a .meta.json
sidecar (frame count per SPAD tag; each quadrant is acquired for the
same duration) for the rate map.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Path to the folder with the S*C '.bin' data files. |
required |
nframes
|
int
|
Number of frames the acquisition was set to record per '.bin' file. |
required |
firmware_version
|
str
|
|
'short_window'
|
exp_time_s
|
float | None
|
Legacy exposure time requested of |
None
|
open_shutter_time_s
|
float | None
|
Photon-sensitive shutter window per frame, in seconds - the rate's
denominator; see 'compute_and_save_hitmap'. The default is |
5e-07
|
max_files
|
int | None
|
Process at most this many files per tag. The default is None. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
Path to the saved '.npy' hitmap. |
Source code in src/dapkel/functions/hitmap_analysis.py
collect_and_plot_hitmap ¶
collect_and_plot_hitmap(path: str | None = None, mode: str = 'timestamp', tag: str = 'ORT', *, npy_path: str | None = None, cmap: str | None = None, rate: bool = True) -> ndarray
Load a saved hitmap '.npy' and save its hitmap plot(s).
Reads the saved array - no re-unpacking - and writes two figures to
results/hitmap: the counts/occupancy map and the photon-rate map,
hits / (nframes * n_files * open_shutter_time_s) in Hz. Both modes normalise the
same way, by the live (shutter-open) time of the acquisition; the
derivation is in docs/guide/hitmap.md. The frame count and live time
come from the '.meta.json' sidecar written at stage 1.
Locate the '.npy' from path or directly via npy_path, which
ignores path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | None
|
Data-folder path used to locate the saved '.npy'. May be None when
|
None
|
mode
|
str
|
Used with |
'timestamp'
|
tag
|
str
|
Used to build the default '.npy' name. The default is |
'ORT'
|
npy_path
|
str | None
|
Explicit path to a saved hitmap '.npy' to plot directly; when given,
|
None
|
cmap
|
str | None
|
Matplotlib colormap name for the hitmaps. The default is None —
use the active style's |
None
|
rate
|
bool
|
Also save the photon-rate hitmap. The default is True. |
True
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The loaded hitmap (counts/occupancy). |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
Raised when the '.npy' hitmap cannot be found. |
ValueError
|
Raised when neither |
Source code in src/dapkel/functions/hitmap_analysis.py
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 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 | |