quantumbox-sdk 2026.10.15.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ # Copied from ../../json-map at build time (single source of truth)
2
+ quantumbox/data/daq1x1.json
3
+ dist/
4
+ *.egg-info/
5
+ .venv/
6
+ __pycache__/
7
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nuclear Instruments s.r.l.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.5
2
+ Name: quantumbox-sdk
3
+ Version: 2026.10.15.3
4
+ Summary: Python SDK for the Nuclear Instruments QuantumBOX: settings, calibration, photon counting and timing
5
+ Project-URL: Homepage, https://www.nuclearinstruments.eu
6
+ Project-URL: Documentation, https://nuclear-instruments.pages.nuclearinstruments.eu/prodotti/daq121-2/application-software/quantumbox/quantumbox-go/
7
+ Project-URL: Bug Tracker, https://support.nuclearinstruments.eu
8
+ Author-email: Nuclear Instruments <info@nuclearinstruments.eu>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: CAEN,NIDAQ121,Nuclear Instruments,QUANTA,QuantumBOX,SciSDK,SiPM,photon counting,photon timing
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Physics
17
+ Requires-Python: >=3.8
18
+ Requires-Dist: numpy>=1.20
19
+ Requires-Dist: scisdk>=1.5
20
+ Provides-Extra: plot
21
+ Requires-Dist: matplotlib>=3.3; extra == 'plot'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # QuantumBOX SDK for Python
25
+
26
+ Python control of the **Nuclear Instruments QuantumBOX** (NIDAQ121): everything the
27
+ QUANTA application does, from your own scripts.
28
+
29
+ - **Settings**: global, per-channel and common trigger configuration, input routing,
30
+ validation, operating mode (photon counting / photon timing), GUI settings files
31
+ - **Oscilloscopes**: trigger scope and one scope per channel, analog and digital traces
32
+ - **Calibration**: hardware energy spectra, zero peak, multi-peak fit, PE thresholds
33
+ - **Photon counting** (List_0): 8-channel energies per trigger, PE classification,
34
+ mean / variance / Fano / g²(0), channel-pair correlations (g₁₁, NRF, γ)
35
+ - **Photon timing** (List_1): per-channel timestamps (leading edge or CFD, ~2 ps),
36
+ inter-arrival and channel-difference histograms
37
+ - **HW counters and coincidence units**
38
+ - **Data saving** in the same formats as QUANTA (bin, csv, txt, npy), and loaders for
39
+ files written by QUANTA
40
+
41
+ ## Requirements
42
+
43
+ - Python 3.8 or later, with `numpy`
44
+ - The **SciSDK** native library installed on the PC (SciSDK setup on Windows, `scisdk`
45
+ package on Linux). The `scisdk` Python wrapper is installed automatically with this
46
+ package.
47
+ - A QuantumBOX reachable over Ethernet (default `192.168.102.199`, port `8888`)
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install quantumbox-sdk
53
+ ```
54
+
55
+ The default firmware register map (`daq1x1.json`) is included: you do not need to pass a
56
+ register file unless your firmware uses a different one.
57
+
58
+ ## Quick start
59
+
60
+ ```python
61
+ from quantumbox import QuantumBox
62
+
63
+ with QuantumBox("192.168.102.199") as box: # port 8888 by default
64
+ print(box.operating_mode) # 'counting' or 'timing'
65
+
66
+ cfg = box.get_global_config()
67
+ cfg.trigger_type = "le" # common / le / cfd
68
+ cfg.qdc_tq_long = 100 # integration time (ns)
69
+ box.set_global_config(cfg)
70
+
71
+ box.update_channels(None, pol=0, qdc_gain_value=1.0) # all channels
72
+
73
+ with box.counting_list() as lst: # photon counting run
74
+ events = lst.acquire(duration_s=5)
75
+ print(len(events), "events, CH0 energies:", events["energy"][:5, 0])
76
+ ```
77
+
78
+ ### Calibration and PE thresholds
79
+
80
+ ```python
81
+ from quantumbox import QuantumBox, SpectrumAnalysisConfig, analyze_spectrum, Thresholds
82
+ import time
83
+
84
+ with QuantumBox("192.168.102.199") as box:
85
+ box.spectra.reset(); box.spectra.start()
86
+ time.sleep(30)
87
+ spectra = box.spectra.read()
88
+ box.spectra.stop()
89
+
90
+ cfg = SpectrumAnalysisConfig(zero_roi_left=2500, zero_roi_right=3400, min_peak_distance=50,
91
+ peak_tail=True, fit_background=True)
92
+ thresholds = Thresholds()
93
+ for ch, counts in spectra.items():
94
+ result = analyze_spectrum(counts, cfg)
95
+ thresholds[ch] = result.threshold_entries()
96
+ thresholds.save("thresholds.json")
97
+ ```
98
+
99
+ ### Photon statistics
100
+
101
+ ```python
102
+ from quantumbox import classify_pe, pe_histogram, pe_statistics, correlation
103
+
104
+ pe0 = classify_pe(events["energy"][:, 0], thresholds[0])
105
+ pe1 = classify_pe(events["energy"][:, 1], thresholds[1])
106
+ print(pe_statistics(pe_histogram(pe0))) # mean, variance, fano, g2
107
+ print(correlation(pe0, pe1)) # g11, nrf, gamma, unbalance, coincidences
108
+ ```
109
+
110
+ ### Photon timing
111
+
112
+ ```python
113
+ from quantumbox import interarrival_histogram, difference_histogram
114
+
115
+ with QuantumBox("192.168.102.199") as box:
116
+ box.set_operating_mode("timing")
117
+ with box.timing_list() as lst: # CFD/LE encoding read from the board
118
+ ev = lst.acquire(duration_s=10)
119
+
120
+ counts, edges = difference_histogram(ev["time_ns"], ev["channel"], 0, 1, range_ns=20, bins=2000, signed=True)
121
+ ```
122
+
123
+ ## Examples
124
+
125
+ The `examples/` folder of the source distribution contains complete scripts:
126
+ connection and settings, oscilloscope capture, calibration, photon counting and timing
127
+ runs with data saving, HW counters and coincidences, offline analysis of saved files.
128
+
129
+ ## Documentation
130
+
131
+ Full user guide: see the **QuantumBOX SDK User Guide** (link in the package page).
132
+
133
+ ## License
134
+
135
+ MIT — © Nuclear Instruments
@@ -0,0 +1,112 @@
1
+ # QuantumBOX SDK for Python
2
+
3
+ Python control of the **Nuclear Instruments QuantumBOX** (NIDAQ121): everything the
4
+ QUANTA application does, from your own scripts.
5
+
6
+ - **Settings**: global, per-channel and common trigger configuration, input routing,
7
+ validation, operating mode (photon counting / photon timing), GUI settings files
8
+ - **Oscilloscopes**: trigger scope and one scope per channel, analog and digital traces
9
+ - **Calibration**: hardware energy spectra, zero peak, multi-peak fit, PE thresholds
10
+ - **Photon counting** (List_0): 8-channel energies per trigger, PE classification,
11
+ mean / variance / Fano / g²(0), channel-pair correlations (g₁₁, NRF, γ)
12
+ - **Photon timing** (List_1): per-channel timestamps (leading edge or CFD, ~2 ps),
13
+ inter-arrival and channel-difference histograms
14
+ - **HW counters and coincidence units**
15
+ - **Data saving** in the same formats as QUANTA (bin, csv, txt, npy), and loaders for
16
+ files written by QUANTA
17
+
18
+ ## Requirements
19
+
20
+ - Python 3.8 or later, with `numpy`
21
+ - The **SciSDK** native library installed on the PC (SciSDK setup on Windows, `scisdk`
22
+ package on Linux). The `scisdk` Python wrapper is installed automatically with this
23
+ package.
24
+ - A QuantumBOX reachable over Ethernet (default `192.168.102.199`, port `8888`)
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install quantumbox-sdk
30
+ ```
31
+
32
+ The default firmware register map (`daq1x1.json`) is included: you do not need to pass a
33
+ register file unless your firmware uses a different one.
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ from quantumbox import QuantumBox
39
+
40
+ with QuantumBox("192.168.102.199") as box: # port 8888 by default
41
+ print(box.operating_mode) # 'counting' or 'timing'
42
+
43
+ cfg = box.get_global_config()
44
+ cfg.trigger_type = "le" # common / le / cfd
45
+ cfg.qdc_tq_long = 100 # integration time (ns)
46
+ box.set_global_config(cfg)
47
+
48
+ box.update_channels(None, pol=0, qdc_gain_value=1.0) # all channels
49
+
50
+ with box.counting_list() as lst: # photon counting run
51
+ events = lst.acquire(duration_s=5)
52
+ print(len(events), "events, CH0 energies:", events["energy"][:5, 0])
53
+ ```
54
+
55
+ ### Calibration and PE thresholds
56
+
57
+ ```python
58
+ from quantumbox import QuantumBox, SpectrumAnalysisConfig, analyze_spectrum, Thresholds
59
+ import time
60
+
61
+ with QuantumBox("192.168.102.199") as box:
62
+ box.spectra.reset(); box.spectra.start()
63
+ time.sleep(30)
64
+ spectra = box.spectra.read()
65
+ box.spectra.stop()
66
+
67
+ cfg = SpectrumAnalysisConfig(zero_roi_left=2500, zero_roi_right=3400, min_peak_distance=50,
68
+ peak_tail=True, fit_background=True)
69
+ thresholds = Thresholds()
70
+ for ch, counts in spectra.items():
71
+ result = analyze_spectrum(counts, cfg)
72
+ thresholds[ch] = result.threshold_entries()
73
+ thresholds.save("thresholds.json")
74
+ ```
75
+
76
+ ### Photon statistics
77
+
78
+ ```python
79
+ from quantumbox import classify_pe, pe_histogram, pe_statistics, correlation
80
+
81
+ pe0 = classify_pe(events["energy"][:, 0], thresholds[0])
82
+ pe1 = classify_pe(events["energy"][:, 1], thresholds[1])
83
+ print(pe_statistics(pe_histogram(pe0))) # mean, variance, fano, g2
84
+ print(correlation(pe0, pe1)) # g11, nrf, gamma, unbalance, coincidences
85
+ ```
86
+
87
+ ### Photon timing
88
+
89
+ ```python
90
+ from quantumbox import interarrival_histogram, difference_histogram
91
+
92
+ with QuantumBox("192.168.102.199") as box:
93
+ box.set_operating_mode("timing")
94
+ with box.timing_list() as lst: # CFD/LE encoding read from the board
95
+ ev = lst.acquire(duration_s=10)
96
+
97
+ counts, edges = difference_histogram(ev["time_ns"], ev["channel"], 0, 1, range_ns=20, bins=2000, signed=True)
98
+ ```
99
+
100
+ ## Examples
101
+
102
+ The `examples/` folder of the source distribution contains complete scripts:
103
+ connection and settings, oscilloscope capture, calibration, photon counting and timing
104
+ runs with data saving, HW counters and coincidences, offline analysis of saved files.
105
+
106
+ ## Documentation
107
+
108
+ Full user guide: see the **QuantumBOX SDK User Guide** (link in the package page).
109
+
110
+ ## License
111
+
112
+ MIT — © Nuclear Instruments
@@ -0,0 +1,48 @@
1
+ """Connect, read the configuration, change settings, save and reload a settings file.
2
+
3
+ python 01_connect_and_configure.py --ip 192.168.102.199
4
+ """
5
+
6
+ from quantumbox import QuantumBox, TriggerConfig
7
+ from _common import parser
8
+
9
+ args = parser(__doc__).parse_args()
10
+
11
+ with QuantumBox(args.ip, register_file=args.register_file) as box:
12
+ print("Connected:", box, "| SciSDK", box.library_version())
13
+ print("Operating mode:", box.operating_mode)
14
+
15
+ # ── global settings (GUI: Settings → Global) ──
16
+ g = box.get_global_config()
17
+ print(f"Trigger: {g.trigger_type}, algorithm: {g.algorithm}, pole zero: {g.pole_zero_enabled}")
18
+ g.algorithm = "qdc"
19
+ g.trigger_type = "common" # common trigger for photon counting
20
+ g.qdc_pre_int = 10 # ns
21
+ g.qdc_tq_long = 100 # integration window, ns
22
+ box.set_global_config(g)
23
+
24
+ # common trigger: leading edge on CH0
25
+ box.set_trigger_config(TriggerConfig(source="le", input_channel=0, polarity="pos",
26
+ threshold=3500, hysteresis=100, inhibit_ns=200))
27
+
28
+ # validation: OR of channels 0..3
29
+ box.set_channel_mask([0, 1, 2, 3])
30
+ box.set_validation("or")
31
+
32
+ # ── per-channel settings (GUI: Settings → Channels) ──
33
+ box.update_channels(None, pol=0, qdc_gain_value=1.0) # all channels
34
+ ch2 = box.get_channel_config(2)
35
+ ch2.hysteresis = 50
36
+ ch2.baseline_samples = 256
37
+ box.set_channel_config(ch2)
38
+ for c in box.get_channel_configs()[:3]:
39
+ print(f"CH{c.index}: gain={c.qdc_gain_value:.3f} thr={c.thrs_high} hyst={c.hysteresis} "
40
+ f"baseline={c.baseline_samples} samples")
41
+
42
+ # input router: CH4 reads physical input B_2
43
+ box.set_input_routing(4, 1)
44
+
45
+ # ── settings file, compatible with the GUI (Toolbar → Load settings) ──
46
+ box.save_settings("my_settings.json")
47
+ box.load_settings("my_settings.json")
48
+ print("Settings saved to my_settings.json and re-applied")
@@ -0,0 +1,49 @@
1
+ """Capture a channel oscilloscope (analog IN / QDC IN and digital traces) and plot it.
2
+
3
+ python 02_oscilloscope.py --ip 192.168.102.199 --channel 2
4
+ """
5
+
6
+ from quantumbox import QuantumBox
7
+ from quantumbox.oscilloscope import select_second_trace
8
+ from _common import parser
9
+
10
+ p = parser(__doc__)
11
+ p.add_argument("--channel", default="2", help="0..7 or 'trigger'")
12
+ p.add_argument("--decimator", type=int, default=1)
13
+ args = p.parse_args()
14
+ channel = args.channel if args.channel == "trigger" else int(args.channel)
15
+
16
+ with QuantumBox(args.ip, register_file=args.register_file) as box:
17
+ if channel != "trigger":
18
+ select_second_trace(box, channel, "qdc") # second trace: QDC IN (or 'cfd')
19
+ with box.oscilloscope(channel) as scope:
20
+ scope.configure(decimator=args.decimator, pretrigger=100,
21
+ trigger_mode="self" if channel == "trigger" else "ext")
22
+ scope.start()
23
+ trace = scope.read(timeout_s=5)
24
+ scope.stop()
25
+
26
+ if trace is None:
27
+ raise SystemExit("No trigger within 5 s")
28
+
29
+ print(f"{trace.analog.shape[1]} samples, {trace.time_ns[-1]:.0f} ns window, "
30
+ f"IN min/max {trace.analog[0].min()}/{trace.analog[0].max()}")
31
+ print("digital tracks:", ", ".join(trace.digital))
32
+
33
+ try:
34
+ import matplotlib.pyplot as plt
35
+ except ImportError:
36
+ raise SystemExit("pip install matplotlib to plot")
37
+
38
+ fig, (ax, axd) = plt.subplots(2, 1, sharex=True, figsize=(10, 6), gridspec_kw={"height_ratios": [3, 1]})
39
+ labels = ["IN", "QDC IN" if channel != "trigger" else "CFD"]
40
+ for i, a in enumerate(trace.analog):
41
+ ax.plot(trace.time_ns, a, label=labels[i] if i < len(labels) else f"A{i}")
42
+ ax.set_ylabel("ADC")
43
+ ax.legend()
44
+ for k, (name, d) in enumerate(trace.digital.items()):
45
+ axd.step(trace.time_ns, d * 0.8 + k, where="post", label=name)
46
+ axd.set_yticks([k + 0.4 for k in range(len(trace.digital))], list(trace.digital))
47
+ axd.set_xlabel("Time (ns)")
48
+ plt.tight_layout()
49
+ plt.show()
@@ -0,0 +1,53 @@
1
+ """Calibration: acquire the hardware spectra, find zero and PE peaks, save PE thresholds.
2
+
3
+ Same workflow as the GUI Calibration tab (Zero Peak → Fit → Save Thresholds).
4
+
5
+ python 03_calibration.py --ip 192.168.102.199 --seconds 30 --zero-roi 2500 3400
6
+ """
7
+
8
+ import time
9
+
10
+ from quantumbox import QuantumBox, SpectrumAnalysisConfig, Thresholds, analyze_spectrum, storage
11
+ from _common import parser
12
+
13
+ p = parser(__doc__)
14
+ p.add_argument("--seconds", type=float, default=30)
15
+ p.add_argument("--zero-roi", type=float, nargs=2, default=[0, 4000], help="ROI containing the zero peak")
16
+ p.add_argument("--channels", type=int, nargs="*", default=list(range(8)))
17
+ p.add_argument("--tails", action="store_true", help="fit heavy tails + background (non-Gaussian peaks)")
18
+ p.add_argument("--out", default="thresholds.json")
19
+ args = p.parse_args()
20
+
21
+ with QuantumBox(args.ip, register_file=args.register_file) as box:
22
+ box.spectra.reset()
23
+ box.spectra.start()
24
+ print(f"Acquiring spectra for {args.seconds:.0f} s ...")
25
+ time.sleep(args.seconds)
26
+ spectra = box.spectra.read()
27
+ box.spectra.stop()
28
+
29
+ cfg = SpectrumAnalysisConfig(
30
+ zero_roi_left=args.zero_roi[0], zero_roi_right=args.zero_roi[1], zero_sigma_multiplier=3,
31
+ min_peak_height_fraction=0.02, min_prominence=0.01, min_peak_distance=50, smooth_window=5,
32
+ peak_tail=args.tails, fit_background=args.tails)
33
+
34
+ thresholds = Thresholds.load(args.out) if __import__("os").path.exists(args.out) else Thresholds()
35
+ for ch in args.channels:
36
+ counts = spectra.get(ch)
37
+ if counts is None or counts.sum() == 0:
38
+ print(f"CH{ch}: no data")
39
+ continue
40
+ storage.save_spectrum(f"spectrum_ch{ch}.csv", counts, channel=ch)
41
+ res = analyze_spectrum(counts, cfg)
42
+ if not res.zero_found:
43
+ print(f"CH{ch}: zero peak not found in ROI {args.zero_roi}")
44
+ continue
45
+ entries = res.threshold_entries()
46
+ thresholds.merge(ch, entries) # like 'Save Thresholds' choosing the new values
47
+ print(f"CH{ch}: zero {res.zero_center:.1f} (sigma {res.zero_sigma:.1f}), {len(res.peaks)} peaks, "
48
+ f"chi2/ndf {res.chi_red:.1f}, {len(entries)} PE windows")
49
+ for g in res.peaks[:4]:
50
+ print(f" peak {g.center:8.1f} sigma {g.sigma:6.1f} height {g.amplitude:10.0f}")
51
+
52
+ thresholds.save(args.out)
53
+ print("Thresholds saved to", args.out)
@@ -0,0 +1,59 @@
1
+ """Photon counting run (List_0): save events, compute PE statistics and correlations live.
2
+
3
+ Needs the thresholds from 03_calibration.py.
4
+
5
+ python 04_photon_counting_run.py --ip 192.168.102.199 --seconds 60 --format npy
6
+ """
7
+
8
+ import numpy as np
9
+
10
+ from quantumbox import (QuantumBox, Thresholds, classify_pe, correlation, pe_histogram,
11
+ pe_statistics, region_statistics, storage)
12
+ from _common import parser
13
+
14
+ p = parser(__doc__)
15
+ p.add_argument("--seconds", type=float, default=30)
16
+ p.add_argument("--format", choices=storage.FORMATS, default="bin")
17
+ p.add_argument("--prefix", default="quantumbox_data")
18
+ p.add_argument("--run", type=int, default=1)
19
+ p.add_argument("--thresholds", default="thresholds.json")
20
+ p.add_argument("--pair", type=int, nargs=2, default=[0, 1], help="channel pair for correlations")
21
+ args = p.parse_args()
22
+
23
+ thr = Thresholds.load(args.thresholds)
24
+ channels = thr.channels()
25
+ pe_hist = {ch: np.zeros(64, dtype=np.int64) for ch in channels}
26
+ energy_hist = {ch: np.zeros(65536, dtype=np.int64) for ch in channels}
27
+ pairs = [] # (peA, peB) batches
28
+
29
+
30
+ def process(batch):
31
+ for ch in channels:
32
+ e = batch["energy"][:, ch]
33
+ energy_hist[ch] += np.bincount(e, minlength=65536)[:65536]
34
+ pe_hist[ch] += pe_histogram(classify_pe(e, thr[ch]), max_pe=63)
35
+ a, b = args.pair
36
+ if a in thr and b in thr:
37
+ pairs.append((classify_pe(batch["energy"][:, a], thr[a]), classify_pe(batch["energy"][:, b], thr[b])))
38
+
39
+
40
+ with QuantumBox(args.ip, register_file=args.register_file) as box:
41
+ box.set_operating_mode("counting")
42
+ path = storage.run_file_name(args.prefix, args.run, args.format)
43
+ with storage.CountingWriter(path) as writer, box.counting_list() as lst:
44
+ lst.acquire(duration_s=args.seconds, writer=writer, on_batch=process)
45
+ print(f"{writer.events} events saved to {path} ({lst.bytes_read / 1e6:.1f} MB)")
46
+
47
+ for ch in channels:
48
+ s = pe_statistics(pe_hist[ch])
49
+ print(f"CH{ch}: mean {s['mean']:.3f} var {s['variance']:.3f} Fano {s['fano']:.3f} g2 {s['g2']:.3f}")
50
+ for r in region_statistics(energy_hist[ch], thr[ch])[:4]:
51
+ extra = f" dpp {r['delta_pp']:.1f}+/-{r['delta_pp_err']:.1f}" if "delta_pp" in r else ""
52
+ print(f" PE {r['pe']}: mean {r['mean']:.1f}+/-{r['mean_err']:.2f} std {r['std']:.1f}{extra}")
53
+
54
+ if pairs:
55
+ a = np.concatenate([x for x, _ in pairs])
56
+ b = np.concatenate([y for _, y in pairs])
57
+ c = correlation(a, b)
58
+ print(f"CH{args.pair[0]}-CH{args.pair[1]}: g11 {c['g11']:.4f} NRF {c['nrf']:.4f} gamma {c['gamma']:.4f} "
59
+ f"coincidences {c['coincidences']}")
@@ -0,0 +1,55 @@
1
+ """Photon timing run (List_1): save events and build inter-arrival / channel-difference histograms.
2
+
3
+ python 05_photon_timing_run.py --ip 192.168.102.199 --seconds 30 --pair 0 1
4
+ """
5
+
6
+ import numpy as np
7
+
8
+ from quantumbox import QuantumBox, Thresholds, difference_histogram, interarrival_histogram, storage
9
+ from _common import parser
10
+
11
+ p = parser(__doc__)
12
+ p.add_argument("--seconds", type=float, default=30)
13
+ p.add_argument("--format", choices=storage.FORMATS, default="bin")
14
+ p.add_argument("--mode", choices=["raw", "processed"], default="raw")
15
+ p.add_argument("--per-channel", action="store_true", help="one file per channel")
16
+ p.add_argument("--thresholds", default=None, help="needed for photons in processed mode")
17
+ p.add_argument("--pair", type=int, nargs=2, default=[0, 1])
18
+ p.add_argument("--run", type=int, default=1)
19
+ args = p.parse_args()
20
+
21
+ thr = Thresholds.load(args.thresholds) if args.thresholds else None
22
+
23
+ with QuantumBox(args.ip, register_file=args.register_file) as box:
24
+ box.set_operating_mode("timing")
25
+ with storage.TimingWriter(".", run=args.run, fmt=args.format, mode=args.mode,
26
+ per_channel=args.per_channel, thresholds=thr) as writer, \
27
+ box.timing_list() as lst: # LE/CFD encoding read from the board
28
+ print("Timestamp encoding:", "CFD" if lst.cfd else "leading edge")
29
+ batches = []
30
+
31
+ def keep(batch):
32
+ batches.append(batch)
33
+
34
+ lst.acquire(duration_s=args.seconds, writer=writer, on_batch=keep)
35
+ print(f"{writer.events} events saved:", ", ".join(str(p) for p in writer.paths.values()))
36
+
37
+ events = np.concatenate(batches) if batches else np.empty(0)
38
+ if len(events) == 0:
39
+ raise SystemExit("no events")
40
+
41
+ for ch in np.unique(events["channel"]):
42
+ n = int(np.sum(events["channel"] == ch))
43
+ counts, edges = interarrival_histogram(events["time_ns"], events["channel"], int(ch), range_ns=1e5, bins=1000)
44
+ mean_dt = np.diff(events["time_ns"][events["channel"] == ch]).mean() if n > 1 else float("nan")
45
+ print(f"CH{ch}: {n} events, mean inter-arrival {mean_dt:.0f} ns")
46
+
47
+ a, b = args.pair
48
+ counts, edges = difference_histogram(events["time_ns"], events["channel"], a, b, range_ns=20, bins=4000, signed=True)
49
+ if counts.sum():
50
+ centers = (edges[:-1] + edges[1:]) / 2
51
+ peak = centers[np.argmax(counts)]
52
+ sel = np.abs(centers - peak) < 2
53
+ mu = np.average(centers[sel], weights=counts[sel])
54
+ sigma = np.sqrt(np.average((centers[sel] - mu) ** 2, weights=counts[sel]))
55
+ print(f"t(CH{a}) - t(CH{b}): peak {mu * 1000:.1f} ps, sigma {sigma * 1000:.1f} ps ({counts.sum()} pairs)")
@@ -0,0 +1,28 @@
1
+ """Hardware counters (rates per channel) and hardware coincidence units.
2
+
3
+ python 06_hw_counters_coincidence.py --ip 192.168.102.199 --seconds 10
4
+ """
5
+
6
+ from quantumbox import QuantumBox
7
+ from _common import parser
8
+
9
+ p = parser(__doc__)
10
+ p.add_argument("--seconds", type=float, default=10)
11
+ args = p.parse_args()
12
+
13
+ with QuantumBox(args.ip, register_file=args.register_file) as box:
14
+ # coincidence unit 0: CH0 AND CH1; unit 1: CH2 AND CH3
15
+ coinc = box.coincidence()
16
+ cfg = coinc.read_config()
17
+ cfg.widths = [10] * 8 # trigger pulse width per channel (TRG_C_WIDTH)
18
+ cfg.delays = [0] * 8 # trigger delay per channel (TRG_C_DELAY)
19
+ coinc.apply(cfg)
20
+ coinc.set_unit(0, [0, 1])
21
+ coinc.set_unit(1, [2, 3])
22
+
23
+ start = coinc.counts()
24
+ for r in box.counters().monitor(interval_s=1.0, duration_s=args.seconds):
25
+ rates = " ".join(f"CH{i}:{v:9.0f}" for i, v in enumerate(r["rate_1s"]))
26
+ print(f"t={r['t']:5.1f}s {rates} Hz")
27
+ end = coinc.counts()
28
+ print("Coincidences in the run:", [e - s for s, e in zip(start, end)][:2], "(units 0, 1)")
@@ -0,0 +1,47 @@
1
+ """Offline analysis of files saved by QUANTA or by the SDK — no hardware needed.
2
+
3
+ python 07_offline_analysis.py spectrum_ch2.csv
4
+ python 07_offline_analysis.py quantumbox_data_run1.bin --thresholds thresholds.json
5
+ """
6
+
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+
12
+ from quantumbox import (SpectrumAnalysisConfig, Thresholds, analyze_spectrum, classify_pe,
13
+ pe_histogram, pe_statistics, storage)
14
+
15
+ p = argparse.ArgumentParser(description=__doc__)
16
+ p.add_argument("file")
17
+ p.add_argument("--thresholds", default=None)
18
+ p.add_argument("--zero-roi", type=float, nargs=2, default=[0, 4000])
19
+ p.add_argument("--plot", action="store_true")
20
+ args = p.parse_args()
21
+ path = Path(args.file)
22
+
23
+ if path.name.startswith("spectrum"):
24
+ counts = storage.load_spectrum(path)
25
+ cfg = SpectrumAnalysisConfig(zero_roi_left=args.zero_roi[0], zero_roi_right=args.zero_roi[1],
26
+ min_peak_distance=50, peak_tail=True, fit_background=True)
27
+ res = analyze_spectrum(counts, cfg)
28
+ print(f"zero {res.zero_center:.1f}, {len(res.peaks)} peaks, chi2/ndf {res.chi_red:.1f}")
29
+ for e in res.threshold_entries()[:8]:
30
+ print(f" PE {e.pe}: [{e.low}, {e.high})")
31
+ if args.plot:
32
+ import matplotlib.pyplot as plt
33
+ plt.semilogy(np.maximum(counts, 0.1), label="data")
34
+ plt.semilogy(np.maximum(res.fit_curve, 0.1), label="fit")
35
+ for t in res.thresholds:
36
+ plt.axvline(t, color="orange", lw=0.6, ls="--")
37
+ plt.legend()
38
+ plt.xlabel("ADC bin")
39
+ plt.show()
40
+ else:
41
+ events = storage.load_counting(path)
42
+ print(f"{len(events)} events, {events['timestamp'][-1] * 4e-9 - events['timestamp'][0] * 4e-9:.1f} s")
43
+ if args.thresholds:
44
+ thr = Thresholds.load(args.thresholds)
45
+ for ch in thr.channels():
46
+ s = pe_statistics(pe_histogram(classify_pe(events["energy"][:, ch], thr[ch])))
47
+ print(f"CH{ch}: mean PE {s['mean']:.3f}, Fano {s['fano']:.3f}, g2 {s['g2']:.3f}")
@@ -0,0 +1,10 @@
1
+ """Shared command line options for the examples."""
2
+
3
+ import argparse
4
+
5
+
6
+ def parser(description: str) -> argparse.ArgumentParser:
7
+ p = argparse.ArgumentParser(description=description)
8
+ p.add_argument("--ip", default="192.168.102.199", help="QuantumBOX address (IP or IP:PORT)")
9
+ p.add_argument("--register-file", default=None, help="firmware register map (default: bundled daq1x1.json)")
10
+ return p