Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.3.0] - 2026-08-09¶
Changed¶
- BREAKING: nothing assumes a 10 MHz reference clock any more. The ring
oscillator sets the TDC code width (~71.6 ps,
core.timing.CODE_S) and the reference clock sets the full scale, so the number of codes scales as 1/f — measured on the 2026-08-07 rate-vs-clock set as 275 codes at 50 MHz, 1379 at 10 MHz, 2761 at 5 MHz and 6914 at 2 MHz, i.e. 1/f to within 2 %. The old_PERIOD_PS = 100_000.0,_MAX_CODES = 1536,osc_period_ps = 100000.0and_SUPPORT_PS = 200000.0constants are gone. Both stage-1 entry points (calculate_and_save_timestamp_differences,calculate_and_save_delta_counts) and everytdc_calibrationentry point now takeopen_shutter_time_s— one reference-clock period, in seconds — with no default, because the TDC's full scale is that window and nothing downstream is right without it. - BREAKING: every time parameter is in seconds, named with an
_ssuffix.bin_width_ps→bin_width_s,plot_window_ps→plot_window_s,fit_window_ps→fit_window_s,time_unit_ps→time_unit_s,support_ps→support_s,grid_ps→grid_s,peak_window_ps→peak_window_s,zoom_window_ps→zoom_window_s,window_ps→window_s,period_ps→open_shutter_time_s,acq_window→open_shutter_time_s,exp_time→exp_time_s,frame_time→frame_time_s,live_time→live_time_s. There is no compatibility shim: an old keyword raisesTypeErrornaming the new one, which is far safer than silently reading10e3as 10 000 seconds. Histogram bin centres, edges and plot axes stay in picoseconds — that is the scale the TDC lives on — and the conversion happens on entry. - BREAKING:
osc_period_psis replaced byfull_scale_son both plot drivers, resolved throughcore.timing.resolve_tdc_full_scale_s: from the calibration LUT first (a LUT is self-describing — its largest entry is the full scale it was built for), then an explicitfull_scale_s, thenopen_shutter_time_swith a warning. That last fallback is only right when the shutter is exactly one clock period; a longer shutter folds the timestamps into the clock period, which is why the 2026-08-05 run showed a ±100 ns triangle behind a 500 ns shutter. - BREAKING:
calc_diff.calculate_differences(delta_window=...)is nowdelta_window_native=..., and it is deliberately not in seconds: it is compared againstpixel_timestamps, which are ps with a LUT and raw codes without, so the unit is the caller's to know. The drivers takedelta_window_sand convert. Same forbackground_subtraction.compute_lagged_differences. - BREAKING: renamed keys in returned dicts and
.meta.jsonsidecars —granularity_ps→granularity_s,cell_ps→cell_s,grid_ps→grid_s,support_ps→support_s,window_ps→window_s,bin_width_ps→bin_width_s, andtime_unit_ps/bin_width_ps/plot_window_psinside the histogram signature. Every cached*_delta_hist.npyand*_delta_counts.npyis therefore invalidated and will re-stream once; that is the intended behaviour, since the values themselves changed unit. _TS_CODE_PScorrected from 77.0 ps to the measured ~71.58 ps (core.timing.CODE_S), and documented as clock-independent: 77.0 came from dividing 100 ns by a rounded "~1300 codes", and was 7.5 % high. This changes the time axis of any uncalibrated (unit='code') histogram.- LUTs written by
collect_and_save_lutsnow carry the window in the file name (TDC_LUT_SPAD0_S0_500ns.npy) so per-clock calibrations coexist in one folder, and recordopen_shutter_time_s/full_scale_sin the sidecar.
Fixed¶
dcr_analysisread timestamping data as if it were counting data, which overstated the DCR by 40-50x. Every entry point summed thephoton_countsfield, but under theS*T/ORTprograms there is no photon counter —unpackreturns the low bits of the coarse timestamp in that slot (it says so, andhitmap_analysisalready honoured it). On the 2026-08-07DCR_S0Trun at 24 V this gave a median of 247 kcps, nearly twice the 125 kcps that one hit per 8 µs frame physically allows; the correct occupancy reading is 5.9 kcps. Nothing flagged it, because a map scaled by ~45 still looks like a DCR map.- A calibration LUT from the wrong reference clock was silently discarding
most of the data.
_structure_pixel_timestampsdropped any code past the LUT's range to NaN with no count and no warning — the comment said "treat them as invalid rather than indexing out of bounds", which is safe but mute. On the 2026-08-07 2 MHz data the packaged 10 MHz LUT keeps 21 % of the singles, and since a coincidence needs both arms, ~4 % of the coincidences; the surviving peak still looks entirely plausible. Nowload_lut/load_board_lutraise at load time when the LUT's own full scale does not matchopen_shutter_time_s, and_structure_pixel_timestampswarns with the discarded fraction, the largest code seen and the implied coincidence loss if a mismatched LUT ever gets that far. - Reusing a saved histogram always reported "0 differences in the window".
The message read
meta.get('n', 0), butnlives atmeta["stats"]["n"]. Cosmetic — theinfodict two lines later was already correct.
Added¶
modeon everydcr_analysisentry point —'count'(default,S*C/ORC) sums the photon counter,'timestamp'(S*T/ORT) counts the frames carrying a valid timestamp, matchinghitmap_analysis. It also selects which quadrant filescompute_dcr_64globs (S0T...S3TvsS0C...S3C), and is recorded in the.meta.jsonsidecar.- A tag/mode cross-check.
tag='S0T'withmode='count'is now aValueErrornaming the correct mode, rather than a plausible-looking map with the wrong numbers on it. pileup_correctfor timestamp mode. Timestamping records at most one count per frame, so the raw rate saturates at1 / open_shutter_time_s(125 kcps at an 8 µs shutter) and under-reports well before that — the hottest pixel of the 24 V run sits at 60 % of the ceiling and reads 76 kcps instead of 117 kcps.pileup_correct=Trueinverts the Poisson censoring,r = -ln(1 - p) / open_shutter_time_s. Off by default; the drivers print a warning naming the ceiling whenever the map gets within 5 % of it.tests/test_dcr_modes.py— pins the field each mode reads, the tag/mode check, and the pile-up inversion against known Poisson rates.core.timingis now the home of the ORT timing scale, alongside the live-time helpers:CODE_S(the ring's ~71.6 ps LSB, with the measurement it comes from),COARSE_BITS/MAX_TS_CODE,to_ps/to_s,codes_for_full_scaleandresolve_tdc_full_scale_s. It also documents the distinction that caused the confusion: the open shutter time is the photon-sensitive window, the TDC full scale is one clock period, and they coincide only when the shutter is one period.- The reference clock has a floor.
unpackbuilds the code as(coarse - 1) * 8 + (8 - fine)from a 10-bit coarse field, so it saturates at 8184 codes ≈ 586 ns: below ~1.7 MHz the counter wraps and the timestamps are meaningless. 2 MHz already uses 85 % of it.codes_for_full_scalewarns when a requested full scale exceeds this. tdc_calibration.lut_full_scale_s(lut)— read a LUT's own full scale back out of it, which is the trustworthy source for the triangle half-base.compute_code_histogramsizes its code axis fromopen_shutter_time_sand its overflow warning now names the fraction lost and the largest code seen, rather than only the bound it exceeded.
0.2.0 - 2026-08-05¶
Added¶
functions.background_subtraction— accidental-background removal by frame shifting. An SPDC pair is always in one frame, so pairing pixel A of frame i with pixel B of frame i+k samples the accidentals only; subtracting that from the same-frame histogram cancels the ORT triangle bin-for-bin, with no fit model. Five functions and no I/O:compute_lagged_differences,frames_per_lag,subtract_background,integrate_residual(the model-free coincidence count) andplot_background_subtraction(three panels, the middle one being the evidence that the pedestal really was accidental). Seedocs/guide/background_subtraction.md.subtract_backgroundon all four delta-t entry points, feather path and counts path alike. Stage 1 (calculate_and_save_delta_counts,calculate_and_save_timestamp_differences) also histograms/records the frame-shifted pairs; stage 2 (collect_and_plot_delta_counts,collect_and_plot_timestamp_differences) fits and plots the residual instead of the raw histogram, writes a three-panel diagnostic beside it, and reportsfit["model_free"]— a coincidence count with no peak or background model in it. Default False, which leaves every artifact, resume signature and figure exactly as they were. Only valid withbackground='flat': there is no triangle left to fit, and that is checked before the expensive pass rather than after it.
The two stage-1 paths produce bit-identical residuals (tested, and
verified on a real 12-file ORT run), so the choice is only about cost: the
counts artifact grows one grid plane per lag (0.5 MB → 4.6 MB on a 1000-file
run), the feather grows one column set per lag (1 GB → ~9 GB). Lag 0 stays the
plain measurement in both — plane 0 of the grid, and the bare "r,c-r,c"
columns of the feather, where the shifted ones are "<pair>@lag<k>" and are
never pooled into the coincidence histogram. compute_and_save_delta_histogram
gained the parameter too and histograms every lag in one streaming pass.
- core.pairs — pair_list, pair_labels and pair_label, the pixel-pair
enumeration and the "ra,ca-rb,cb" label format that every coincidence
artifact is indexed by. delta_t had it twice (once inline in the feather
path, once as _pair_labels); both now call this.
- core.timing.resolve_cycle_time — the length of one acquisition cycle,
from the firmware setting rather than from a file on disk. Two versions:
'short_exposure' (the cycle is always 9 µs; the photon-sensitive exposure
inside it is 50–500 ns, typically 100 ns) and 'long_exposure' (the caller
sets X and the firmware adds 9 µs, so the cycle is exp_time + 9 µs). The
older 'short_window' / 'full_window' names are accepted as aliases.
Changed¶
- The hitmap rate is now a photon rate per wall-clock second, in Hz, for both
modes:
hits / (nframes * n_files * cycle). Every frame is one acquisition cycle, so that product is the elapsed time of the run.countmode no longer divides by the exposure window (which gave counts-per-live-second, a number ~90x larger), andtimestampmode no longer resolves the period fromframe_rate_cnt.txt.compute_and_save_hitmap/compute_and_save_hitmap_64takefirmware_version('short_exposure'default, or'long_exposure'withexp_time) and record the resolved cycle in the sidecar; the figure is now produced unconditionally, sincenframesis required and the cycle always resolves.acq_windowis renamedexposure_windowand is recorded and reported as a duty cycle only — it does not enter the rate. The in-exposure rate is the plotted figure timescycle / exposure, and that factor is printed. frame_rate_cnt.txtis no longer read anywhere. It is written by the acquisition exe, which is not ours, so no analysis result may depend on it.core.timing.resolve_frame_timeno longer opens it — the frame period now comes fromexp_time + 9 µsor the 9 µs free-running base — and with the file read gone,resolve_frame_timeno longer needs afolderargument andresolve_live_timeno longer needs one either (both signatures changed;dcr_analysisupdated accordingly). The file itself is left on disk untouched.
For the record, the counter is self-consistent, and an earlier draft of this
entry wrongly called it unreliable on the grounds that several folders share a
value. They share it because they share settings. It is a 200 MHz tick count
over the whole run and every value on disk fits
ticks = nframes * N + 138 + window/5ns exactly, which independently confirms
nframes = 10 000, reports the exposure register back ((ticks mod nframes)
− 138 = window / 5 ns, matching the folder names), and flags runs that died
early. It also says the real cycle is 9.685–9.770 µs, not 9.000 µs, so the
nominal normalisation used here runs ~7.8 % high — a deliberate,
documented choice rather than a measurement. Note the sample scripts
extract_DCR.m, extract_dcr.py and dcr_hitmap.py read it as ticks per
frame with no division, which is wrong by a factor of nframes.
nframesis now required wherever.binfiles are unpacked. It was optional, andNonemeant "derive it from the file size". A frame count is an acquisition setting, not a property of a file that happens to be on disk: deriving it silently turns a truncated or still-being-written file into a different (wrong) normalisation, and every rate the package reports divides by it. It is now a required argument onhitmap_analysis.compute_hitmap/compute_hitmap_64/compute_and_save_hitmap/compute_and_save_hitmap_64(second positional, matchingdcr_analysis), oncrosstalk_analysis.compute_crosstalk/compute_and_save_crosstalk/collect_and_plot_crosstalk/combine_directional_crosstalk, and as a required keyword oncore.reduce.accumulate_frames,data_quality.collect_time_mat/plot_time_code_histogram,tdc_calibration.compute_code_histogram/compute_tdc_lut/collect_and_save_lutsanddelta_t.calculate_and_save_timestamp_differences/calculate_and_save_delta_counts.core.io.frames_in_fileis unchanged and still there for the case where a file's size really is the only record left — it is now the caller's explicit choice rather than a silent default, and its docstring warns that the relation between the frame count an acquisition is asked for and the size of the '.bin' it produces is not established (every sample file is a whole power-of-two number of frames, which a 10 000-frame run should not produce).hitmap_analysis.collect_and_plot_hitmapno longer recovers a missing frame count from the.binsizes either: a stage-1.npywhose sidecar has nototal_framesgets the counts figure but no rate figure, rather than a rate normalised by a guess. Recompute it to get one.- The
nframesused is part of the resume signature for the checkpointed counts pass (delta_t.calculate_and_save_delta_counts), replacingvalid_minthere. It decides how much of each file is read, so a checkpoint written with one value must not be continued with another. Existing partial artifacts will not resume across this change; they restart rather than mixing.
Removed¶
valid_min. Validity of an ORT timestamp istime_series > 0—unpackleaves non-fired slots at<= 0— so this was a knob whose only correct value was its default, and a wrong one silently drops or invents photons. The threshold is now written into the code at each site, with the reason. Dropped fromhitmap_analysis,data_quality.collect_time_mat,tdc_calibrationanddelta_t(both stage-1 paths).-
part_size_mbandkeep_partsondelta_t.calculate_and_save_timestamp_differences. The part cap is now the module constant_PART_MB(still 64 MB): sharding is a memory strategy that cannot change the numbers, so no call site needs to tune it. The parts are now always kept. They are the crash insurance, and the read side streams a part folder exactly as it streams the combined feather — a histogram over fixed edges is a sum over record batches, and a sum does not care which file a batch came from — so working from the parts gives identical counts while skipping the combine pass, which reads and rewrites every byte of the run. Delete the folder by hand once the combined feather has been checked. -
TODO.md, beside this file and published as Open items — the questions that have deliberately not been decided, with the trade-off and what evidence would settle each. This file records what was decided; that one records what was left open, so a known gap cannot read as an oversight. It opens with the delta-t feather keeping both its parts and its combined copy, which doubles the disk a finished run occupies.
Fixed¶
- The README's quick start passed
acq_window=1000todcr_analysis, which takes seconds — a live time seven orders of magnitude too long, and so a DCR quietly reported as ~1e-7 of the truth. It now reads100e-9, with the unit spelled out. Nothing in the package changed; the example was wrong. - The changelog's link block had no
[0.1.4]entry and still pointed[Unreleased]atv0.1.3.
0.1.4 - 2026-08-02¶
Added¶
- A second stage 1 that writes no feather at all.
delta_t.calculate_and_save_delta_countsruns the same acquisition loop but bins the differences onto a fixed native grid as it finds them, saving only the counts toprocessed/<name>_delta_counts.npy. The artifact is sized by the grid rather than the data: 106 MB for 256 pairs whether the run is 100 files or 10 000, against a 3.22 GB feather for the same run and ~135 GB for a long one. Every pair is then plottable at once, andbin_width_ps/plot_window_psstop being compute-time decisions. For raw codes it is exactly lossless —delta_codeis an integer, so counts-per-code is a re-encoding. It is offered beside the feather path, not instead of it: no per-event data survives, so re-calibrating with a different LUT, re-cuttingdelta_windowand anything needing individual differences are all gone. delta_t.load_delta_countsanddelta_t.rebin_delta_countsread the grid back and reduce it onto any coarser histogram in memory, anddelta_t.collect_and_plot_delta_countsis the matching plot driver. It returns(counts, centers, fit)and shares its fitting and drawing code withcollect_and_plot_timestamp_differences, so a difference between the two paths can only come from the histogram.- Crash insurance for the counts path is one file rather than a part folder:
the grid is rewritten every
flush_everyfiles and the next call resumes. Two runs cannot interleave, so the part-mixing hazard does not arise. The sidecar carriestotal_in_grid, and a checkpoint that does not sum to it — a crash between writing the array and writing the sidecar — is discarded rather than resumed, which would otherwise re-count files.
Fixed¶
- The "streaming" feather read was not streaming a single-batch feather.
pa.ipc.open_file(path)materialises a whole record batch the moment one is asked for. That is invisible on a feather assembled from 64 MB parts, but every feather written before the parts rewrite - and anything_write_delta_featherwrites directly - is a single batch, so the read loaded the entire file: measured at +794 MB for a 0.79 GB feather, meaning a 9.42 GB one asked for 9.42 GB in one allocation, on a 16.8 GB machine. Every batch-reading site now memory-maps instead, soget_batchcosts nothing and the OS pages in only the columns touched. The same 9.42 GB, 320-column feather now histograms in 18 s with no resident growth. - Re-binning onto an incommensurate width put a sawtooth in the histogram.
A 71 ps bin over a 7.7 ps grid holds 9 or 10 cells depending on where it
lands, so a smooth distribution acquires an ~11% bin-to-bin ripple; on real
data it moved the peak bin by 8%.
collect_and_plot_delta_countsnow snaps the width to an odd whole number of cells and the window to a whole number of bins. Odd matters: bins are centred on zero, so their edges lie at half-integer multiples of the width, which for an odd cell count fall on cell boundaries and for an even one fall through cell centres — splitting a cell at every boundary. Verified bin-for-bin on a 3.22 GB run: zero difference from the feather path at 9, 11 and 13 cells per bin, ~1% at 8, 10 and 12. Snapping also puts zero exactly on a bin centre, which the feather path'sarange(-W - w/2, ...)edges only manage when the window happens to be a whole multiple of the width.
Known¶
- The feather path's outermost bin at each end is under-filled: it cuts data
at
abs(delta) <= plot_window_psbut draws edges half a bin beyond it. The counts path fills those bins properly, which is most of the ~1% difference in fittedsigmabetween the two. Not changed here — it would alter every existing feather-path result.
0.1.3 - 2026-08-02¶
Fixed¶
- Delta-t parts from two runs could be pooled into one feather. Part
numbering restarts at 0 on every run, so a run that died at part 300 and was
restarted for 120 parts had its first 120 overwritten and its last 180 left
in place — and
combine_delta_t_partsglobbed the whole folder, pooling all - The combined feather then counted a slice of the run twice, inflating
both the file and the coincidence total, and skewing the peak against the
accidental background. Each run now tags its parts with its own id and
records them in a manifest (rewritten on every flush, so a dead run is still
described),
combine_delta_t_partsrefuses to pool several runs and takesrun=to pick one, andcalculate_and_save_timestamp_differencesrefuses to start while another run's parts are in the folder rather than interleaving with them. Nothing is deleted on that refusal — the parts are the crash insurance.
Changed¶
- Delta-t parts are now named
<name>_delta_t_<run>_<i>.featherand are accompanied by a<name>_delta_t_<run>_manifest.json. Folders written before this keep working: their untagged parts read as a singlelegacyrun.
0.1.2 - 2026-08-02¶
Added¶
delta_t.compute_and_save_delta_histogram— streams a delta-t feather down to its histogram one Arrow record batch at a time, so a combined feather far larger than RAM (a 10 000-file run reaches ~135 GB) can be read at all. The counts are saved toprocessed/<name>_delta_hist.npywith a.meta.jsonsidecar recording the binning and a fingerprint of the sources, so replotting or changing the background model costs a small load instead of another full pass. The sidecar also reports what share of the cells read were padding.feather_pathmay now be a*_delta_t_partsfolder, and a run whose combined feather is missing falls back to its parts automatically — the giant combined file is no longer needed to plot.
Changed¶
delta_t.collect_and_plot_timestamp_differencesnow returns(counts, centers, fit)instead of(deltas_ps, fit), and never materialises the differences. Breaking: returning the pooled array was the reason the function could not open a full-size feather. The histogram is bin-for-bin identical to what the old load-everything path produced — both fit models and the plot only ever consumed(bin_centers, counts).- Promoting
[Unreleased]to a numbered release is a manual edit ofCHANGELOG.md, documented step by step in the README. Nothing has ever written the changelog automatically; the removed script only did that edit for you. tools/changelog.pyis now usable from a Jupyter interactive window, so the release notes can be previewed without a terminal. Running the file there defines its functions instead of firing the CLI —__name__really is"__main__"in a kernel, so argparse used to parse ipykernel's own argv and die withSystemExit: 2.main()also takes an explicitargv, andnotes_forraisesChangelogErrorinstead of callingsys.exit. Onlymain()converts that to a non-zero exit, so the gatepublish.ymlrelies on is unchanged.- The "no section for this version" failure now spells out the heading and link edits to make, and warns that deleting a release leaves its tag behind. That message is what the person cutting a failed release actually reads.
Removed¶
tools/release.pyand its tests. It automated one two-line edit toCHANGELOG.mdand was the only part of the release process that wanted a terminal.tools/changelog.pystays:publish.ymlruns it on the runner to refuse a tag whose version has no changelog section, and to fill the release body.
Fixed¶
- The changelog's link block still pointed
[Unreleased]at av0.2.0that was never tagged, carried a[0.2.0]link with no matching section, and had no[0.1.1]entry.
0.1.1 - 2026-08-02¶
Added¶
core.store.confirm_rewrite— the guard everyrewrite=Truenow routes through. Silent on a fresh run; when data really is about to be destroyed it names each target with its size and timestamp, then counts downstore.REWRITE_DELAY_S(5 s) so Ctrl-C can still save an overnight acquisition.delta_t's tworewriteparameters use it.delta_t.combine_delta_t_parts— assemble a run's part files into its combined feather. Recovers a run that died partway through: every part flushed before the crash is complete and readable.delta_t.calculate_and_save_timestamp_differencesgainedpart_size_mb(default 64 MB) andkeep_parts(default True).tools/release.py— promotes the changelog's[Unreleased]section to a numbered, dated, linked release. The publish workflow reads the changelog as it was at the tagged commit, so notes left under[Unreleased]can never ship; forgetting to rename the section meant deleting and recreating the tag.
Changed¶
- Delta-t stage 1 streams to disk instead of accumulating the whole run.
Differences are flushed to
processed/<dataset>_delta_t_parts/<dataset>_delta_t_<i>.featheras they are found and concatenated one Arrow record batch at a time, so the loop's memory ceiling is one part plus one unpacked file regardless of file count. Over ~10 000 files the old path failed twice over — the buffers exhausted RAM, and the single final write then asked pyarrow for a multi-GB contiguous allocation on top of what was already held. combine_delta_t_featherspools batch-by-batch rather than reading every feather into a DataFrame first, so combining many runs no longer needs the whole pooled data set resident twice.- Delta-t feathers pad short pair columns with Arrow nulls rather than NaN
values. Both read back as NaN through
to_pandas, so this is invisible downstream and older feathers still read correctly; it just keeps padding distinguishable from data when parts are counted.
0.1.0 - 2026-08-01¶
The package grows from "unpack and plot DCR" into the full Kelpie analysis
set, on a shared three-stage structure: raw .bin -> processed/ ->
results/. Stage 2 never re-reads raw data, so retrying a colormap costs a
load instead of a full unpack.
Added¶
dapkel.core, the shared substrate every analysis is built from:core.io- locate and size the raw.binfiles (find_bin_files,frames_in_file,BYTES_PER_FRAME).core.timing- frame period and photon-sensitive live time (resolve_frame_time,resolve_live_time,CLK_PERIOD,FREE_RUNNING_US).core.reduce- fold files into one sensor map (accumulate_frames) and interleave four quadrants into the full sensor (assemble_64).core.plots- the two figure shapes the package draws (sensor_map,sorted_distribution) plus NaN-awaremap_stats.core.store- the single place that decides where artifacts land (processed_dir,results_dir,save_map,load_map,save_figure).
crosstalk_analysis- per-direction and combined optical cross-talk maps.hitmap_analysis- occupancy and photon-rate maps, per quadrant and full sensor.tdc_calibration- statistical code density test producing per-pixel TDC lookup tables, with DNL diagnostics. Board LUTs ship with the wheel underdapkel.paramsand load throughload_board_lut.calc_diffanddelta_t- timestamp differences between pixel groups (1v1andall_pairs), written to Feather, with Gaussian and Gaussian-plus-triangle peak fits for jitter.data_quality- run this on a fresh acquisition. A TDC that never stopped produces data that looks entirely normal to every other analysis;plot_time_code_histogrammakes that failure visible.- Save-and-reload paths for every analysis, so figures can be redrawn from
processed/without touching the raw files. - Documentation site (mkdocs-material + mkdocstrings) under
docs/, with a guide page per analysis carrying the physics derivations, and API pages generated from the docstrings. - Test suite: structural guards on the public API surface (
__all__completeness, no duplicated helpers across analyses, no cross-module private imports, no hand-built artifact paths) and geometry tests tying the SPAD layout, cross-talk directions and quadrant interleave together. dapkel.__version__, apy.typedmarker (PEP 561), and ruff configuration.- CI: tests and lint on Linux and Windows across Python 3.10 and 3.13, a strict docs build, and GitHub Pages deployment.
- Automated releases. Publishing a GitHub release validates the tag, refuses to
ship a version with no changelog entry, checks that the built version matches
the tag, uploads to PyPI, and fills the release body from
CHANGELOG.md.tools/changelog.pyrenders those notes locally before you tag.
Changed¶
- Artifact layout. Stage-1 data now goes to
processed/and figures toresults/<kind>/, replacing the ad-hocsenpop_data/,delta_ts_data/andresults/tdc_calibration/folders. Existing data is not migrated automatically - move it by hand if you want old runs picked up. unpackis roughly 6x faster: packed pixel fields are extracted with shifts and masks instead of exploding every bit into a largeuint8array. Output is bit-for-bit identical to the previous implementation and to the MATLAB reference.matplotlibis no longer pinned to an exact version (==3.10.8->>=3.8), which was making dapkel uninstallable alongside anything else that needed a different matplotlib.- Docstrings are shorter at the call site; the physics derivations moved to the
documentation site. Full
Parameters/Returnssections are kept, sohelp()still answers "what does this argument take". - Figure sizing follows one rule: square sensor maps are 16x16, everything else
takes
figure.figsizefrom the active style.
Fixed¶
- Micropixel geometry. SPADs are indexed clockwise inside a 2x2
macropixel (
S0 S1 / S3 S2), not row-major. The full-sensor interleave indcr_analysishadS2CandS3Cswapped, so quadrant data landed on the wrong cells of every(64, 64)map. Cross-talk was affected twice over: the vertical and diagonal measurements were both mislabelled and placed on each other's cells. map_statsand the heatmaps are NaN-aware. A single non-finite cell used to drivevmaxto NaN, collapsing a whole sensor map to two colours and printingmedian nanin the title.delta_tderived its figure directory one level too shallow, nestingresults/inside the data folder. Bothdelta_tandhitmap_analysisnow route throughcore.store.requirements.txtwas missingkomorebi_mpland pinned versions that contradictedpyproject.toml.
0.0.1 - 2026-07-05¶
Initial commit.
Added¶
- Functions for unpacking raw, binary data and analyzing dark count rate.