sipm-characterisation 0.1.0
SiPM characterisation for ePIC — IV/DCR/gain, laser, readout, irradiation
Loading...
Searching...
No Matches
Project design log

Project-wide design reference + hub. This file is the main entry point for anyone reading the architectural thinking behind sipm-characterisation. It also holds the triage queue produced by the first post-consolidation code audit (see TODOs). When a component grows enough to warrant its own design notes, add a satellite DISCUSSION.md under that directory and index it from the hub table below.

Section What it holds Removal trigger
Satellite discussions Pointers to per-area DISCUSSION.md files. None yet — everything is in this hub. Satellite file added / removed → update the hub.
Triage taxonomy Convention for tagging items (Design / TODO / Attention / Feature). Taxonomy change → update the chapter.
Design discussions Open questions — decision needed before code change. Each D-XX is self-contained. Decision made → entry deleted or moved to TODOs.
TODOs Concrete code-work items (audit bugs). No design pending. Fix lands on main → row removed.
Attention points Latent caveats that don't need a design discussion but do need a heads-up. Caveat resolved or captured as a design question / TODO.
Feature queue Discussed-but-not-designed additions. Feature shipped or promoted to D-XX.
Coding conventions Naming + style reference. Reference material — no removal trigger.

Satellite discussions — hub

No satellites yet. Add a row here when one lands. Convention: one satellite per top-level component (laser/, readout/, irradiation/, …), holding the narrative (why, options, history); the hub just indexes it.


Triage taxonomy

Items in this log carry one of four tags:

Tag Lives in Means
D-XX Design discussions Architectural question; decision needed before code.
TODO TODOs Concrete fix; no design pending.
Attention Attention points Latent caveat; not actionable yet, but readers should know.
F-XX Feature queue Future addition; not yet designed enough to be D-XX.

When in doubt: if it changes the layout or how components are configured, it's D-XX. If it's "this line is wrong, fix it", it's a TODO. If it's "watch out, this looks fine but isn't", it's Attention.


Design discussions

D-01 — Data-path configuration strategy

Files: source/database.C (database::basedir), readout/analysis_utils.h (ALCOR_DATA_DIR / ALCOR_WORK_DIR), plus many macros with hardcoded /Users/nrubini/... absolute paths (see TODO-1).

Observation: three different mechanisms coexist for locating data — a global basedir string, environment variables, and hardcoded absolute paths. A new user cannot run most macros without editing source, and readout/ silently falls back to ./ when the env vars are unset (see Attention-1).

Options:

Option Behaviour Trade-off
A — env vars everywhere One SIPM_DATA_DIR, read in one helper Consistent; requires touching every entry point
B — single config file A conf/paths.toml (or similar) read at startup Explicit + reproducible; adds a parser dependency
C — required parameter Every macro takes data_dir as an argument (the core already does) No global state; verbose at call sites

Recommendation: Option A for a quick unification (the get_environment_variable helper already exists in readout/analysis_utils.h), migrating the hardcoded paths first. Decision needed: single env var name + whether to keep basedir as a thin wrapper over it.

D-02 — Consolidation of duplicated macros and shared types

Files: laser/tgraphs/macros/tgraphs_to_ttree.C vs …_ALL.C (~220 near-identical lines each); daq/macros/IV.C draw functions; the ALCOR enum type_t / hit struct redeclared in slewrate/macros/{deltat,signal_basic,signal_aps}.C, alcor.h, finetune.h (and they have already divergedalcor.h uses t_hit, the others hit_t); the four config maps duplicated across macros/IV_plots_for_paper.C and macros/my_test_macro_on_Tdep.C.

Observation: the duplication is now a correctness risk, not just bloat — the ALCOR hit definitions differ between copies.

Options: (A) leave as-is — macros are independent and rarely touched; (B) hoist shared types into one header per area (slewrate/macros/alcor.h already exists and is the natural home) and parameterise the near-duplicate pipelines on their one differing knob.

Recommendation: B for the ALCOR hit/enum definitions (genuine divergence hazard) and the tgraphs_to_ttree pair; defer the cosmetic macro-config duplication. Decision needed: confirm alcor.h as the single source of truth for the hit model.

D-03 — Scope and fate of <tt>legacy/</tt>

Files: legacy/ (salvaged macros: proton_scan.C, annealing_scan.C, get_board_history.C, check_overvoltage_noise.C, temp_scan_test_fit.C, memmert_noise.C, finetune.h).

Observation: these are unique macros salvaged from retired forks. They are reference-only, may not compile against the current source//utils/, and are not wired into anything.

Options: (A) keep indefinitely as reference; (B) port the still-useful ones (proton_scan, annealing_scan) into macros/ and delete the rest; (C) drop the directory entirely once their content is confirmed superseded.

Recommendation: B — promote what's still wanted, drop the throwaways. Decision needed: which macros are still scientifically relevant.


TODOs — concrete fixes in the queue

Surfaced by the first post-consolidation audit (2026-06). Grouped by kind; each row is a self-contained fix.

Compile-blockers / won't-load

# File:line Problem
C-1 macros/TIFPA_2023_results.C:147,153 TCanvas/TH1F re-declared in the same scope (already declared L80/L86) — does not compile.
C-2 macros/quick_look.C:3 Parameter std::string database shadows the database:: namespace used at L9–11 — does not compile. Rename the parameter.
C-3 macros/annealing_excursus.C:66 vector::push_back(a, b) — two args; should be push_back({a, b}).
C-4 macros/my_test_macro_on_pPDE_meas.C:9–12 Malformed stub (bare expressions, empty for ()).
C-5 daq/keithley/stability.py:34, transition.py:45 Call ky.source_config(...) which does not exist (only source_measure_config) → immediate AttributeError.
C-6 daq/keithley/keithley.py:201 query_source_config(name) references undefined nameNameError when verbose=True.
C-7 laser/waveform/timewalk.C:1 (+ signal.C) #include "qGaus.C" — file absent from the tree; macros won't load. Vendor qGaus or remove the dependency.
C-8 slewrate/macros/finetune.h:53–67 get_guesstimate_correction_from_pulser uses undeclared Tree/nev/current_hit.channel, push_back on a std::pair, inverted !std::find(...)!=end() — abandoned scaffolding; finish or delete.

Physics-correctness (changes results)

# File:line Problem
P-1 macros/IV_plots_for_paper.C:1061, macros/my_test_macro_on_Tdep.C:19,81 Uncertainty on ln(I) computed as log(val)·(err/val); correct is err/val. Distorts Arrhenius-fit errors.
P-2 source/database.C:328 substr(find("TEMP")+4, find("TEMP")+7) — 2nd arg is a length, not an end index; over-reads the temperature tag. Intended length is 3.
P-3 source/database.C:1096–1098 RMS written to the average graph index 0+iSensor*2 (should be 1+…); the average point is overwritten by the RMS.
P-4 source/database_laser.C:518 bkg:: stability graph filled with current_pPDE instead of current_bkg (copy-paste) — background plot actually shows pPDE.
P-5 macros/temperature_scan.C:128–133 Error loop never resets iTer, so every iteration reads the same last element — RMS computed from one point repeated.
P-6 utils/graphutils.C:101 eval_with_errors loops < GetN()-1, never evaluating the last interval; overvoltages near the top of a scan silently return -1.
P-7 readout/fine_analysis.C:340–341 Unconditional break on the first line of the offset-calibration loop body — the refinement never runs; offset never computed. (L352 convergence if(){} is also an empty body.)

Crash / robustness

# File:line Problem
R-1 source/database_laser.C:325–335 Get("hDelta") used with no null check → null-deref; TFile leaked on the zombie path.
R-2 utils/graphutils.C:211–219 themin[ibin-1] where ibin = TProfile::Fill(...) returns -1 on under/overflow → out-of-bounds write. Guard ibin >= 1.
R-3 utils/utility.h:465–478 get_intercept_tf1 is an infinite while(true) (no break); get_intercept / get_intercept_pol1_pol1 return zero-initialised results → measure_breakdown_0 intercept always {0,0}.
R-4 daq/keithley/keithley.py:97–98 sock.recv() returns bytes; terminator test ‘data[-1] != ’
'(int vs str) is always true. Decode or compare againstb'
'. \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone"> R-5 \ilinebr </td> <td class="markdownTableBodyNone">irradiation/src/DetectorConstruction.hh:56\ilinebr </td> <td class="markdownTableBodyNone">mSystemAngleuninitialised but read inConstruct()(L271) before any messenger sets it → garbage rotation. Init= 0.. \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone"> R-6 \ilinebr </td> <td class="markdownTableBodyNone">irradiation/src/RootIO.hh:41\ilinebr </td> <td class="markdownTableBodyNone">mSensorInstanceIDuninitialised; compared inFill()beforeConstruct()sets it. Init= -1. \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone"> R-7 \ilinebr </td> <td class="markdownTableBodyNone">irradiation/src/main.cc:18,44\ilinebr </td> <td class="markdownTableBodyNone">G4UIExecutiveconstructed twice; the first leaks and may grab the terminal. Construct only in the interactive branch. \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone"> R-8 \ilinebr </td> <td class="markdownTableBodyNone">readout/fastMiniFrame.C:25–27,noise_analysis.C:27,readout/fine_analysis.C:73\ilinebr </td> <td class="markdownTableBodyNone">TFile::Open/Get("alcor")dereferenced with no null check → segfault on missing file/tree. \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone"> R-9 \ilinebr </td> <td class="markdownTableBodyNone">slewrate/macros/alcor.h:68–85,dcalib.C:66\ilinebr </td> <td class="markdownTableBodyNone">load_alcor_data_treeno null check onfin/tin;dcalib.Cdivides bynwhich can be 0 (NaN/inf). \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone"> R-10 \ilinebr </td> <td class="markdownTableBodyNone">daq/macros/IV.C:437–504\ilinebr </td> <td class="markdownTableBodyNone">Vbdfitwhile(true)loops have no iteration cap (non-converging fit hangs);method 2/default silently returns0.;std::sqrtof negative reverse current → NaN. \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone"> R-11 \ilinebr </td> <td class="markdownTableBodyNone">source/ureadout_dcr_get.C:6,source/makeiv.C:14–31,readout/fine_analysis.h:82\ilinebr </td> <td class="markdownTableBodyNone">TFileopened and never closed (leak per call); the open-to-test-existence pattern also prints a ROOT error each call — prefergSystem->AccessPathName(). \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone"> R-12 \ilinebr </td> <td class="markdownTableBodyNone">laser/waveform/lib/wavedesc.h:175\ilinebr </td> <td class="markdownTableBodyNone">file.read(&DATA, WAVE_ARRAY_1)reads a header-supplied (signed) length into a fixed 1 MiB buffer with no bound check — buffer overflow on a corrupt header. Validate0 ≤ WAVE_ARRAY_1 ≤ sizeof(DATA). \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone"> R-13 \ilinebr </td> <td class="markdownTableBodyNone">laser/tgraphs/macros/tgraphs_to_ttree.C:32(+_ALL) \ilinebr </td> <td class="markdownTableBodyNone">for (iPnt … iPnt <= GetN())reads one past the last point. Should be< GetN(). \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone"> R-14 \ilinebr </td> <td class="markdownTableBodyNone">readout/fine_analysis.h:467–473\ilinebr </td> <td class="markdownTableBodyNone"> DegeneratekCalibrationMin == kCalibrationMax == 0(skipped channel) → division by zero in the calibrated-phase normalisation. GuardkNormalisation == 0`.

Quick correctness nits

# File:line Problem
N-1 utils/utility.h:74 round_digits uses std::ceil (always rounds up). If round-to-nearest is intended, use std::round; else rename.
N-2 utils/utility.h:165 Form("%s_gausfilter_n%i_a%i", …) passes 2 args for 3 specifiers (alpha_parameter not forwarded) — undefined output.
N-3 laser/waveform/lib/utility.h:84–85 get_graph_max seeds result_x/result_y from GetY()/GetX() (transposed) — wrong for a 1-point graph.
N-4 readout/analysis_utils.h:70 rollover_to_coarse accumulated in int (coarse + rollover*32768) overflows for long spills — use Long64_t.
N-5 readout/fine_analysis.C:288, analysis_example.C:86 chips[4]/chips[5] via operator[] insert phantom map entries before the size check — use count()/find().
N-6 source/database.C:355 Annealing-minutes parse assumes an ‘'h’precedes'm'; pure-minutes tags compute a garbage offset vianpos. \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone"> N-7 \ilinebr </td> <td class="markdownTableBodyNone">readout/fine_analysis.h:434,486\ilinebr </td> <td class="markdownTableBodyNone"> Comment@Chiara …is parsed by Doxygen as an unknown command (build warning). Reword toChiaraor@Chiara`.

Attention points — latent issues to be careful about

  • Attention-1 — silent ./ data fallback. readout/analysis_utils.h:60–63 returns "./" when ALCOR_DATA_DIR / ALCOR_WORK_DIR are unset, so input paths silently become relative and "no data found" looks like an empty run. Emit a warning when the variable is missing.
  • Attention-2 — ROOT object ownership in interactive macros. Many macros leak TCanvas/TH*/TGraph and leave TFiles open by ROOT-idiom. Harmless for interactive use, but any macro promoted to a reusable function must SetDirectory(0) histograms kept after the file closes and manage lifetimes.
  • Attention-3 — hard-indexed graph points. source/database_laser.C:516–519 reads GetPointY(2) assuming ≥3 vbias points; fewer points read out of range (ROOT returns a silent sentinel). Guard GetN() before fixed-index access.
  • Attention-4 — irradiation/CMakeLists.txt minimum is 2.8, far below what modern Geant4 / C++17 needs. Bump to ≥ 3.16 and move cmake_minimum_required before project(). (Tracked as F-04.)
  • Attention-5 — daq/keithley/keithley.py global socket/state. The module uses module-level sock/commands globals — non-reentrant and untestable. (Tracked as F-03.)

Feature queue

  • F-01 — Top-level orchestration. There is no top-level build or run entry point; each component is launched by hand. A thin scripts/ driver (or a rootlogon.C that sets include paths) would lower the barrier for new users.
  • F-02 — Deduplicate the pipeline macros. Parameterise tgraphs_to_ttree.C / …_ALL.C and the IV.C draw functions on their single differing knob (see D-02).
  • F-03 — Wrap the Keithley socket in a class. Replace the module globals in keithley.py with a Keithley object holding the socket; pass it to the scan scripts. Makes the DAQ reentrant and testable.
  • F-04 — Modernise the Geant4 build. Bump irradiation/CMakeLists.txt to a realistic CMake/Geant4 baseline; consider lazy creation of the 100×5 absorber messenger objects.
  • F-05 — Use numpy.linspace for scan grids. daq/keithley/ivscan.py:66 uses np.arange with a float step (endpoint instability); fwdscan.py already patches around it. Unify on linspace for reproducible bias grids.

Coding conventions

  • Language: C++ run as ROOT macros (.C / .h), Python 3 for DAQ, Geant4 C++ for the simulation. No top-level build — components are independent.
  • Namespaces: core analysis lives under database::, graphutils::, utility::. Do not shadow these with local variables (see C-2).
  • Data location: must come from configuration (env var / parameter), never a hardcoded absolute path (see D-01 / TODO-P-row paths).
  • ROOT ownership: detach histograms with SetDirectory(0) if they outlive their TFile; close every TFile you open; null-check every TFile::Open and Get.
  • Documentation: every file gets a Doxygen @file block; public functions get @brief / @param / @return. The API site is built by .github/workflows/docs.yml.
  • History policy: granular WIP on dev/feature branches; squash-merge one meaningful commit per feature onto main. Full pre-v1 history is kept only until the first v1.0.0 tag, then a clean-root reset.