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.
- quantumbox_sdk-2026.10.15.3/.gitignore +7 -0
- quantumbox_sdk-2026.10.15.3/LICENSE +21 -0
- quantumbox_sdk-2026.10.15.3/PKG-INFO +135 -0
- quantumbox_sdk-2026.10.15.3/README.md +112 -0
- quantumbox_sdk-2026.10.15.3/examples/01_connect_and_configure.py +48 -0
- quantumbox_sdk-2026.10.15.3/examples/02_oscilloscope.py +49 -0
- quantumbox_sdk-2026.10.15.3/examples/03_calibration.py +53 -0
- quantumbox_sdk-2026.10.15.3/examples/04_photon_counting_run.py +59 -0
- quantumbox_sdk-2026.10.15.3/examples/05_photon_timing_run.py +55 -0
- quantumbox_sdk-2026.10.15.3/examples/06_hw_counters_coincidence.py +28 -0
- quantumbox_sdk-2026.10.15.3/examples/07_offline_analysis.py +47 -0
- quantumbox_sdk-2026.10.15.3/examples/_common.py +10 -0
- quantumbox_sdk-2026.10.15.3/pyproject.toml +45 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/__init__.py +41 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/analysis.py +687 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/config.py +271 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/counters.py +110 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/data/daq1x1.json +3171 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/device.py +393 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/errors.py +15 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/lists.py +231 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/oscilloscope.py +159 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/spectrum.py +101 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/storage.py +329 -0
- quantumbox_sdk-2026.10.15.3/quantumbox/timing.py +108 -0
|
@@ -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
|