dadl-quantum 0.0.2__py3-none-any.whl
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.
- dadl/__init__.py +18 -0
- dadl/analysis/__init__.py +6 -0
- dadl/analysis/chsh.py +69 -0
- dadl/analysis/visibility.py +47 -0
- dadl/cli.py +86 -0
- dadl/closed_form.py +72 -0
- dadl/coincidences.py +32 -0
- dadl/config.py +42 -0
- dadl/deadtime.py +38 -0
- dadl/errors.py +17 -0
- dadl/guardrails.py +155 -0
- dadl/result.py +59 -0
- dadl/sim.py +133 -0
- dadl_quantum-0.0.2.dist-info/METADATA +36 -0
- dadl_quantum-0.0.2.dist-info/RECORD +17 -0
- dadl_quantum-0.0.2.dist-info/WHEEL +4 -0
- dadl_quantum-0.0.2.dist-info/entry_points.txt +2 -0
dadl/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""DADL: Detector-Aware Data Layer for quantum optics experiments."""
|
|
2
|
+
|
|
3
|
+
from dadl.errors import ConfigError, DadlError, GuardrailRefusal, InsufficientCounts
|
|
4
|
+
from dadl.guardrails import Policy
|
|
5
|
+
from dadl.result import Estimate, LabelledValue
|
|
6
|
+
|
|
7
|
+
__version__ = "0.0.2"
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ConfigError",
|
|
11
|
+
"DadlError",
|
|
12
|
+
"Estimate",
|
|
13
|
+
"GuardrailRefusal",
|
|
14
|
+
"InsufficientCounts",
|
|
15
|
+
"LabelledValue",
|
|
16
|
+
"Policy",
|
|
17
|
+
"__version__",
|
|
18
|
+
]
|
dadl/analysis/chsh.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""CHSH from fixed-grid coincidences.
|
|
2
|
+
|
|
3
|
+
Headline S uses raw counts only. Every coincidence-normalized S is labelled
|
|
4
|
+
"fair sampling assumed", because normalizing by coincidences post-selects on
|
|
5
|
+
detection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from dadl.coincidences import grid_coincidences
|
|
16
|
+
from dadl.errors import InsufficientCounts
|
|
17
|
+
from dadl.guardrails import Policy
|
|
18
|
+
from dadl.result import Estimate, LabelledValue
|
|
19
|
+
|
|
20
|
+
FAIR_SAMPLING = "fair sampling assumed (post-selected on coincidences)"
|
|
21
|
+
|
|
22
|
+
#: Below this many coincidences per setting the Gaussian error is not trusted.
|
|
23
|
+
MIN_COUNTS_PER_SETTING = 100
|
|
24
|
+
|
|
25
|
+
SIGNS: dict[tuple[str, str], int] = {("a", "b"): 1, ("a", "b'"): -1, ("a'", "b"): 1, ("a'", "b'"): 1}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Window:
|
|
30
|
+
"""A coincidence window. `source` must be "plan" for Bell-type analyses."""
|
|
31
|
+
|
|
32
|
+
width_ps: int
|
|
33
|
+
source: str = "plan"
|
|
34
|
+
delays_ps: dict[str, int] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def correlator(tags: dict[str, np.ndarray], window: Window) -> tuple[float, float, int]:
|
|
38
|
+
"""E = (N++ + N-- - N+- - N-+) / N and its variance (1 - E^2) / N."""
|
|
39
|
+
d = window.delays_ps
|
|
40
|
+
|
|
41
|
+
def n(a: str, b: str) -> int:
|
|
42
|
+
return grid_coincidences(tags[a], tags[b], window.width_ps, d.get(a, 0), d.get(b, 0))
|
|
43
|
+
|
|
44
|
+
npp, nmm, npm, nmp = n("A+", "B+"), n("A-", "B-"), n("A+", "B-"), n("A-", "B+")
|
|
45
|
+
total = npp + nmm + npm + nmp
|
|
46
|
+
if total < MIN_COUNTS_PER_SETTING:
|
|
47
|
+
raise InsufficientCounts(f"{total} coincidences in a setting; need at least {MIN_COUNTS_PER_SETTING}.")
|
|
48
|
+
e = (npp + nmm - npm - nmp) / total
|
|
49
|
+
return e, (1.0 - e * e) / total, total
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def chsh(
|
|
53
|
+
runs: dict[tuple[str, str], dict[str, np.ndarray]],
|
|
54
|
+
window: Window,
|
|
55
|
+
policy: Policy | None = None,
|
|
56
|
+
) -> LabelledValue:
|
|
57
|
+
"""Raw CHSH S from one run per setting pair, with fixed windows from the analysis plan."""
|
|
58
|
+
policy = policy or Policy.default()
|
|
59
|
+
policy.check_window("chsh", window.source)
|
|
60
|
+
s = 0.0
|
|
61
|
+
var = 0.0
|
|
62
|
+
for key, sign in SIGNS.items():
|
|
63
|
+
if key not in runs:
|
|
64
|
+
raise ValueError(f"missing run for setting pair {key}")
|
|
65
|
+
e, v, _ = correlator(runs[key], window)
|
|
66
|
+
s += sign * e
|
|
67
|
+
var += v
|
|
68
|
+
labels = (FAIR_SAMPLING,) + policy.labels_for("chsh")
|
|
69
|
+
return LabelledValue(analysis="chsh", raw=Estimate(s, math.sqrt(var)), labels=labels)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Two-photon fringe visibility, with optional accidental subtraction.
|
|
2
|
+
|
|
3
|
+
For Bell-type visibility claims (analysis "bell_visibility") accidental
|
|
4
|
+
subtraction is refused: it can move a non-violating dataset past the 71%
|
|
5
|
+
threshold. For HOM it is allowed, as a labelled secondary value.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
from dadl.guardrails import Policy, threshold_labels
|
|
13
|
+
from dadl.result import Estimate, LabelledValue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _visibility(n_max: float, n_min: float) -> tuple[float, float]:
|
|
17
|
+
total = n_max + n_min
|
|
18
|
+
if total <= 0:
|
|
19
|
+
raise ValueError("counts must be positive")
|
|
20
|
+
v = (n_max - n_min) / total
|
|
21
|
+
# Poisson counts: var(V) = 4 n_max n_min / (n_max + n_min)^3
|
|
22
|
+
return v, math.sqrt(4.0 * n_max * n_min / total**3)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def two_photon_visibility(
|
|
26
|
+
n_max: float,
|
|
27
|
+
n_min: float,
|
|
28
|
+
*,
|
|
29
|
+
analysis: str,
|
|
30
|
+
accidentals: float | None = None,
|
|
31
|
+
policy: Policy | None = None,
|
|
32
|
+
) -> LabelledValue:
|
|
33
|
+
"""Raw visibility, and the accidental-subtracted value only where the policy allows it."""
|
|
34
|
+
policy = policy or Policy.default()
|
|
35
|
+
raw_v, raw_err = _visibility(n_max, n_min)
|
|
36
|
+
corrected: Estimate | None = None
|
|
37
|
+
if accidentals is not None:
|
|
38
|
+
policy.check_correction(analysis, "accidental_subtraction")
|
|
39
|
+
denom = n_max + n_min - 2.0 * accidentals
|
|
40
|
+
if denom <= 0:
|
|
41
|
+
raise ValueError("accidentals exceed the measured counts")
|
|
42
|
+
cor_v = (n_max - n_min) / denom
|
|
43
|
+
corrected = Estimate(cor_v, raw_err * (n_max + n_min) / denom)
|
|
44
|
+
labels = policy.labels_for(analysis) + threshold_labels(
|
|
45
|
+
analysis, raw_v, None if corrected is None else corrected.value
|
|
46
|
+
)
|
|
47
|
+
return LabelledValue(analysis=analysis, raw=Estimate(raw_v, raw_err), corrected=corrected, labels=labels)
|
dadl/cli.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Command-line interface: `dadl quickstart <example>` and `dadl version`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from dadl import __version__
|
|
11
|
+
from dadl.analysis.chsh import Window, chsh
|
|
12
|
+
from dadl.closed_form import chsh_expected_grid
|
|
13
|
+
from dadl.sim import CHANNELS, DetectorParams, PairSource, simulate_chsh
|
|
14
|
+
|
|
15
|
+
#: Phase 1 keeps the example parameters in code; examples/chsh-spad/setup.yaml mirrors them.
|
|
16
|
+
CHSH_SPAD = {
|
|
17
|
+
"seed": 2026,
|
|
18
|
+
"duration_s": 1.0,
|
|
19
|
+
"pair_rate": 1e5,
|
|
20
|
+
"visibility": 0.95,
|
|
21
|
+
"detector": {
|
|
22
|
+
"efficiency": 0.5,
|
|
23
|
+
"dark_rate": 500.0,
|
|
24
|
+
"dead_time_ps": 22_000,
|
|
25
|
+
"jitter_ps": 350.0,
|
|
26
|
+
"afterpulse_prob": 0.005,
|
|
27
|
+
"afterpulse_tau_ps": 50_000.0,
|
|
28
|
+
},
|
|
29
|
+
"window_ps": 2_000,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def quickstart_chsh_spad() -> int:
|
|
34
|
+
p = CHSH_SPAD
|
|
35
|
+
det = DetectorParams(**p["detector"])
|
|
36
|
+
rng = np.random.default_rng(p["seed"])
|
|
37
|
+
runs = simulate_chsh(
|
|
38
|
+
PairSource(pair_rate=p["pair_rate"], visibility=p["visibility"]),
|
|
39
|
+
{ch: det for ch in CHANNELS},
|
|
40
|
+
p["duration_s"],
|
|
41
|
+
rng,
|
|
42
|
+
)
|
|
43
|
+
result = chsh(runs, Window(width_ps=p["window_ps"], source="plan"))
|
|
44
|
+
expected = chsh_expected_grid(
|
|
45
|
+
pair_rate=p["pair_rate"],
|
|
46
|
+
duration_s=p["duration_s"],
|
|
47
|
+
efficiency=det.efficiency,
|
|
48
|
+
dark_rate=det.dark_rate,
|
|
49
|
+
jitter_sigma_ps=det.jitter_ps,
|
|
50
|
+
width_ps=p["window_ps"],
|
|
51
|
+
visibility=p["visibility"],
|
|
52
|
+
)
|
|
53
|
+
print(f"DADL {__version__} quickstart: chsh-spad (simulated data, seed {p['seed']})")
|
|
54
|
+
print(f" Source: {p['pair_rate']:.0e} pairs/s, visibility {p['visibility']}, {p['duration_s']} s per setting")
|
|
55
|
+
print(
|
|
56
|
+
f" Detectors: eta {det.efficiency}, dark {det.dark_rate:.0f} cps, dead time {det.dead_time_ps / 1000:.0f} ns, "
|
|
57
|
+
f"jitter {det.jitter_ps:.0f} ps, afterpulsing {det.afterpulse_prob:.1%}"
|
|
58
|
+
)
|
|
59
|
+
print(f" Fixed window from the analysis plan: {p['window_ps'] / 1000:.1f} ns")
|
|
60
|
+
print()
|
|
61
|
+
print(f" Raw S = {result.raw}")
|
|
62
|
+
print(f" Labels: {'; '.join(result.labels)}")
|
|
63
|
+
print(f" Closed-form expectation (ignores dead time and afterpulsing): S = {expected:.4f}")
|
|
64
|
+
print()
|
|
65
|
+
print(" Next: calibration fits and corrected values arrive in phase 2. See docs/design.md.")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
EXAMPLES = {"chsh-spad": quickstart_chsh_spad}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: list[str] | None = None) -> int:
|
|
73
|
+
parser = argparse.ArgumentParser(prog="dadl", description="Detector-Aware Data Layer for quantum optics experiments")
|
|
74
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
75
|
+
qs = sub.add_parser("quickstart", help="run an example end to end on simulated data")
|
|
76
|
+
qs.add_argument("example", choices=sorted(EXAMPLES))
|
|
77
|
+
sub.add_parser("version", help="print the version")
|
|
78
|
+
args = parser.parse_args(argv)
|
|
79
|
+
if args.command == "version":
|
|
80
|
+
print(__version__)
|
|
81
|
+
return 0
|
|
82
|
+
return EXAMPLES[args.example]()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
sys.exit(main())
|
dadl/closed_form.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Closed-form results used as model-independent test oracles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def nonparalyzable_measured_rate(true_rate: float, tau_s: float) -> float:
|
|
9
|
+
"""m = n / (1 + n tau)."""
|
|
10
|
+
return true_rate / (1.0 + true_rate * tau_s)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def paralyzable_measured_rate(true_rate: float, tau_s: float) -> float:
|
|
14
|
+
"""m = n exp(-n tau)."""
|
|
15
|
+
return true_rate * math.exp(-true_rate * tau_s)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def series_hybrid_measured_rate(true_rate: float, tau_p_s: float, tau_np_s: float) -> float:
|
|
19
|
+
"""m = n exp(-n tau_p) / (1 + n tau_np)."""
|
|
20
|
+
return true_rate * math.exp(-true_rate * tau_p_s) / (1.0 + true_rate * tau_np_s)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def chsh_werner(visibility: float) -> float:
|
|
24
|
+
"""S = 2 sqrt(2) V for a Werner-like Phi+ state at the optimal settings."""
|
|
25
|
+
return 2.0 * math.sqrt(2.0) * visibility
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def chsh_with_accidentals(visibility: float, true_coinc: float, accidentals: float) -> float:
|
|
29
|
+
"""Uniform accidentals scale every correlator, so S = 2 sqrt(2) V C / (C + A)."""
|
|
30
|
+
return chsh_werner(visibility) * true_coinc / (true_coinc + accidentals)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def grid_capture_probability(jitter_sigma_ps: float, width_ps: float) -> float:
|
|
34
|
+
"""Probability that both photons of a pair land in the same fixed slot.
|
|
35
|
+
|
|
36
|
+
Each arm has independent Gaussian jitter of sigma `jitter_sigma_ps`, the
|
|
37
|
+
declared delays are exact, and the pair time is uniform within a slot.
|
|
38
|
+
With D the arrival difference (sigma s = sigma * sqrt(2)) and w the width,
|
|
39
|
+
P = E[(1 - |D| / w)+] = erf(u / sqrt 2) - (2 s / (w sqrt(2 pi))) (1 - exp(-u^2 / 2)), u = w / s.
|
|
40
|
+
"""
|
|
41
|
+
if width_ps <= 0:
|
|
42
|
+
raise ValueError("width must be positive")
|
|
43
|
+
s = jitter_sigma_ps * math.sqrt(2.0)
|
|
44
|
+
if s == 0:
|
|
45
|
+
return 1.0
|
|
46
|
+
u = width_ps / s
|
|
47
|
+
return math.erf(u / math.sqrt(2.0)) - (2.0 * s / (width_ps * math.sqrt(2.0 * math.pi))) * (
|
|
48
|
+
1.0 - math.exp(-0.5 * u * u)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def chsh_expected_grid(
|
|
53
|
+
*,
|
|
54
|
+
pair_rate: float,
|
|
55
|
+
duration_s: float,
|
|
56
|
+
efficiency: float,
|
|
57
|
+
dark_rate: float,
|
|
58
|
+
jitter_sigma_ps: float,
|
|
59
|
+
width_ps: float,
|
|
60
|
+
visibility: float,
|
|
61
|
+
) -> float:
|
|
62
|
+
"""Expected raw S for the simulator with identical detectors, no dead time or afterpulsing.
|
|
63
|
+
|
|
64
|
+
True coincidences per setting: C = N T eta^2 P_capture (summed over the four
|
|
65
|
+
outcome combinations). Accidentals per combination: r^2 w T with singles
|
|
66
|
+
rate r = N eta / 2 + d; uniform across combinations, so they add nothing to
|
|
67
|
+
the correlator numerator.
|
|
68
|
+
"""
|
|
69
|
+
c = pair_rate * duration_s * efficiency**2 * grid_capture_probability(jitter_sigma_ps, width_ps)
|
|
70
|
+
r = pair_rate * efficiency / 2.0 + dark_rate
|
|
71
|
+
a = 4.0 * r * r * (width_ps * 1e-12) * duration_s
|
|
72
|
+
return chsh_with_accidentals(visibility, c, a)
|
dadl/coincidences.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Fixed-grid (clock-locked) coincidence counting.
|
|
2
|
+
|
|
3
|
+
Time is cut into slots of a width declared before the data is seen. A
|
|
4
|
+
coincidence is a slot occupied by both channels after each channel's declared
|
|
5
|
+
delay is removed. This is the only coincidence method allowed for Bell-type
|
|
6
|
+
analyses: windows centred on detection events open the coincidence-time
|
|
7
|
+
loophole.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def occupied_slots(times: np.ndarray, width_ps: int, delay_ps: int = 0) -> np.ndarray:
|
|
16
|
+
if width_ps <= 0:
|
|
17
|
+
raise ValueError("slot width must be positive")
|
|
18
|
+
times = np.asarray(times, dtype=np.int64)
|
|
19
|
+
return np.unique(np.floor_divide(times - np.int64(delay_ps), np.int64(width_ps)))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def grid_coincidences(
|
|
23
|
+
a: np.ndarray,
|
|
24
|
+
b: np.ndarray,
|
|
25
|
+
width_ps: int,
|
|
26
|
+
delay_a_ps: int = 0,
|
|
27
|
+
delay_b_ps: int = 0,
|
|
28
|
+
) -> int:
|
|
29
|
+
"""Number of slots occupied by both channels."""
|
|
30
|
+
slots_a = occupied_slots(a, width_ps, delay_a_ps)
|
|
31
|
+
slots_b = occupied_slots(b, width_ps, delay_b_ps)
|
|
32
|
+
return int(np.intersect1d(slots_a, slots_b, assume_unique=True).size)
|
dadl/config.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Loading setup.yaml files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from dadl.errors import ConfigError
|
|
11
|
+
|
|
12
|
+
SUPPORTED_SCHEMA_VERSIONS = {1}
|
|
13
|
+
REQUIRED_KEYS = ("schema_version", "name", "source", "detectors", "analysis")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_setup(path: str | Path) -> dict[str, Any]:
|
|
17
|
+
"""Load and minimally validate a setup file.
|
|
18
|
+
|
|
19
|
+
Full JSON Schema validation (dadl check-config) and migrations
|
|
20
|
+
(dadl migrate) arrive in a later phase.
|
|
21
|
+
"""
|
|
22
|
+
path = Path(path)
|
|
23
|
+
try:
|
|
24
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
25
|
+
except FileNotFoundError as exc:
|
|
26
|
+
raise ConfigError(f"setup file not found: {path}") from exc
|
|
27
|
+
except yaml.YAMLError as exc:
|
|
28
|
+
raise ConfigError(f"setup file is not valid YAML: {exc}") from exc
|
|
29
|
+
if not isinstance(data, dict):
|
|
30
|
+
raise ConfigError("setup file must be a mapping at the top level")
|
|
31
|
+
missing = [k for k in REQUIRED_KEYS if k not in data]
|
|
32
|
+
if missing:
|
|
33
|
+
raise ConfigError(f"setup file is missing keys: {', '.join(missing)}")
|
|
34
|
+
if data["schema_version"] not in SUPPORTED_SCHEMA_VERSIONS:
|
|
35
|
+
raise ConfigError(
|
|
36
|
+
f"schema_version {data['schema_version']!r} is not supported; "
|
|
37
|
+
f"supported: {sorted(SUPPORTED_SCHEMA_VERSIONS)}"
|
|
38
|
+
)
|
|
39
|
+
window = data["analysis"].get("window", {})
|
|
40
|
+
if window.get("source") != "plan":
|
|
41
|
+
raise ConfigError("analysis.window.source must be 'plan': windows are declared before acquisition")
|
|
42
|
+
return data
|
dadl/deadtime.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Dead-time filters (pure-Python reference; the Rust core has the fast versions).
|
|
2
|
+
|
|
3
|
+
Timestamps are int64 picoseconds, sorted ascending.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def apply_nonparalyzable(times: np.ndarray, tau_ps: int) -> np.ndarray:
|
|
12
|
+
"""Drop arrivals within `tau_ps` of the last recorded event."""
|
|
13
|
+
if tau_ps < 0:
|
|
14
|
+
raise ValueError("dead time must be non-negative")
|
|
15
|
+
times = np.asarray(times, dtype=np.int64)
|
|
16
|
+
if tau_ps == 0 or times.size == 0:
|
|
17
|
+
return times.copy()
|
|
18
|
+
out = np.empty_like(times)
|
|
19
|
+
n = 0
|
|
20
|
+
next_allowed: int | None = None
|
|
21
|
+
for t in times.tolist():
|
|
22
|
+
if next_allowed is None or t >= next_allowed:
|
|
23
|
+
out[n] = t
|
|
24
|
+
n += 1
|
|
25
|
+
next_allowed = t + tau_ps
|
|
26
|
+
return out[:n]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def apply_paralyzable(times: np.ndarray, tau_ps: int) -> np.ndarray:
|
|
30
|
+
"""Record an arrival only if the previous arrival (recorded or not) is at least `tau_ps` earlier."""
|
|
31
|
+
if tau_ps < 0:
|
|
32
|
+
raise ValueError("dead time must be non-negative")
|
|
33
|
+
times = np.asarray(times, dtype=np.int64)
|
|
34
|
+
if times.size == 0:
|
|
35
|
+
return times.copy()
|
|
36
|
+
gaps = np.diff(times)
|
|
37
|
+
keep = np.concatenate(([True], gaps >= tau_ps))
|
|
38
|
+
return times[keep]
|
dadl/errors.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Typed errors. The same types are raised by the library and, later, over MCP."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DadlError(Exception):
|
|
5
|
+
"""Base class for all DADL errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GuardrailRefusal(DadlError):
|
|
9
|
+
"""A correction, window or analysis choice is invalid for the analysis type."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InsufficientCounts(DadlError):
|
|
13
|
+
"""Counts are below the estimator's validity threshold."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigError(DadlError):
|
|
17
|
+
"""A setup or policy file is missing, malformed or of an unsupported version."""
|
dadl/guardrails.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Guardrail policy.
|
|
2
|
+
|
|
3
|
+
Guardrails are refusals in code, not warnings. Each analysis type declares
|
|
4
|
+
which corrections may appear (always as a labelled secondary value, never as
|
|
5
|
+
the headline) and where its coincidence window may come from.
|
|
6
|
+
|
|
7
|
+
Users may loosen a rule, but never silently: an override needs a written
|
|
8
|
+
reason, and any policy whose content differs from the shipped default is
|
|
9
|
+
labelled "policy modified" on every result. In later phases an override also
|
|
10
|
+
needs a human approval token, and agents cannot write policy files.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import math
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
from dadl.errors import GuardrailRefusal
|
|
21
|
+
|
|
22
|
+
POLICY_MODIFIED = "policy modified"
|
|
23
|
+
THRESHOLD_AFTER_CORRECTION = "threshold crossed only after correction"
|
|
24
|
+
UNVALIDATED_ANALYSIS = "unvalidated analysis"
|
|
25
|
+
|
|
26
|
+
#: Thresholds that carry a physical claim, per analysis type.
|
|
27
|
+
THRESHOLDS: dict[str, float] = {
|
|
28
|
+
"chsh": 2.0,
|
|
29
|
+
"bell_visibility": 1.0 / math.sqrt(2.0),
|
|
30
|
+
"hom_visibility": 0.5,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class AnalysisRule:
|
|
36
|
+
allowed_corrections: frozenset[str]
|
|
37
|
+
window_sources: frozenset[str]
|
|
38
|
+
|
|
39
|
+
def to_json(self) -> dict[str, list[str]]:
|
|
40
|
+
return {
|
|
41
|
+
"allowed_corrections": sorted(self.allowed_corrections),
|
|
42
|
+
"window_sources": sorted(self.window_sources),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
#: Applied to any analysis type the policy does not know, e.g. plugin analyses.
|
|
47
|
+
STRICTEST = AnalysisRule(allowed_corrections=frozenset(), window_sources=frozenset({"plan"}))
|
|
48
|
+
|
|
49
|
+
DEFAULT_RULES: dict[str, AnalysisRule] = {
|
|
50
|
+
# Bell-type: raw counts only, fixed windows declared in the analysis plan.
|
|
51
|
+
"chsh": STRICTEST,
|
|
52
|
+
"bell_visibility": STRICTEST,
|
|
53
|
+
# HOM and tomography: corrections allowed as labelled secondary values.
|
|
54
|
+
"hom_visibility": AnalysisRule(
|
|
55
|
+
allowed_corrections=frozenset({"accidental_subtraction", "afterpulse"}),
|
|
56
|
+
window_sources=frozenset({"plan"}),
|
|
57
|
+
),
|
|
58
|
+
"tomography": AnalysisRule(
|
|
59
|
+
allowed_corrections=frozenset({"accidental_subtraction"}),
|
|
60
|
+
window_sources=frozenset({"plan"}),
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class Override:
|
|
67
|
+
analysis: str
|
|
68
|
+
correction: str
|
|
69
|
+
reason: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class Policy:
|
|
74
|
+
rules: dict[str, AnalysisRule] = field(default_factory=lambda: dict(DEFAULT_RULES))
|
|
75
|
+
overrides: tuple[Override, ...] = ()
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def default(cls) -> Policy:
|
|
79
|
+
return cls()
|
|
80
|
+
|
|
81
|
+
# -- content hashing ---------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def fingerprint(self) -> str:
|
|
84
|
+
"""SHA-256 of the canonical rule content (overrides applied)."""
|
|
85
|
+
content = {name: rule.to_json() for name, rule in sorted(self._effective_rules().items())}
|
|
86
|
+
blob = json.dumps(content, sort_keys=True, separators=(",", ":")).encode()
|
|
87
|
+
return hashlib.sha256(blob).hexdigest()
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_modified(self) -> bool:
|
|
91
|
+
return self.fingerprint() != _DEFAULT_FINGERPRINT
|
|
92
|
+
|
|
93
|
+
# -- loosening ---------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def allow(self, analysis: str, correction: str, reason: str) -> Policy:
|
|
96
|
+
"""Return a new policy that allows `correction` for `analysis`.
|
|
97
|
+
|
|
98
|
+
A written reason is mandatory. Results under the new policy carry the
|
|
99
|
+
"policy modified" label.
|
|
100
|
+
"""
|
|
101
|
+
if not reason or not reason.strip():
|
|
102
|
+
raise ValueError("A policy override needs a written reason.")
|
|
103
|
+
return Policy(rules=dict(self.rules), overrides=self.overrides + (Override(analysis, correction, reason.strip()),))
|
|
104
|
+
|
|
105
|
+
# -- checks ------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def rule_for(self, analysis: str) -> AnalysisRule:
|
|
108
|
+
return self._effective_rules().get(analysis, STRICTEST)
|
|
109
|
+
|
|
110
|
+
def check_correction(self, analysis: str, correction: str) -> None:
|
|
111
|
+
if correction not in self.rule_for(analysis).allowed_corrections:
|
|
112
|
+
raise GuardrailRefusal(
|
|
113
|
+
f"Correction '{correction}' is not allowed for '{analysis}'. "
|
|
114
|
+
"Bell-type headline numbers use raw counts only; see docs/design.md, "
|
|
115
|
+
"'Scientific guardrails'."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def check_window(self, analysis: str, window_source: str) -> None:
|
|
119
|
+
if window_source not in self.rule_for(analysis).window_sources:
|
|
120
|
+
raise GuardrailRefusal(
|
|
121
|
+
f"A coincidence window from '{window_source}' is not allowed for '{analysis}'. "
|
|
122
|
+
"Windows must be fixed and declared in the analysis plan before acquisition; "
|
|
123
|
+
"windows chosen from the data open the coincidence-time loophole."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def labels_for(self, analysis: str) -> tuple[str, ...]:
|
|
127
|
+
labels: list[str] = []
|
|
128
|
+
if self.is_modified:
|
|
129
|
+
labels.append(POLICY_MODIFIED)
|
|
130
|
+
if analysis not in DEFAULT_RULES:
|
|
131
|
+
labels.append(UNVALIDATED_ANALYSIS)
|
|
132
|
+
return tuple(labels)
|
|
133
|
+
|
|
134
|
+
def _effective_rules(self) -> dict[str, AnalysisRule]:
|
|
135
|
+
rules = dict(self.rules)
|
|
136
|
+
for o in self.overrides:
|
|
137
|
+
base = rules.get(o.analysis, STRICTEST)
|
|
138
|
+
rules[o.analysis] = AnalysisRule(
|
|
139
|
+
allowed_corrections=base.allowed_corrections | {o.correction},
|
|
140
|
+
window_sources=base.window_sources,
|
|
141
|
+
)
|
|
142
|
+
return rules
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
_DEFAULT_FINGERPRINT = Policy().fingerprint()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def threshold_labels(analysis: str, raw: float, corrected: float | None) -> tuple[str, ...]:
|
|
149
|
+
"""Label a result whose corrected value crosses a physical threshold that raw does not."""
|
|
150
|
+
threshold = THRESHOLDS.get(analysis)
|
|
151
|
+
if threshold is None or corrected is None:
|
|
152
|
+
return ()
|
|
153
|
+
if raw < threshold <= corrected:
|
|
154
|
+
return (THRESHOLD_AFTER_CORRECTION,)
|
|
155
|
+
return ()
|
dadl/result.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""The labelled result type.
|
|
2
|
+
|
|
3
|
+
A DADL result always carries its raw value, an optional corrected value and
|
|
4
|
+
the labels that qualify it. It deliberately refuses to become a bare float, so
|
|
5
|
+
a corrected number cannot be quoted without its raw value and labels by
|
|
6
|
+
accident (see docs/design.md, "Labels travel with numbers").
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Estimate:
|
|
16
|
+
"""A value with its one-sigma standard error."""
|
|
17
|
+
|
|
18
|
+
value: float
|
|
19
|
+
stderr: float
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return f"{self.value:.4f} ± {self.stderr:.4f}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class LabelledValue:
|
|
27
|
+
"""Raw value first, optional corrected value, and labels."""
|
|
28
|
+
|
|
29
|
+
analysis: str
|
|
30
|
+
raw: Estimate
|
|
31
|
+
corrected: Estimate | None = None
|
|
32
|
+
labels: tuple[str, ...] = field(default_factory=tuple)
|
|
33
|
+
artifact_ids: tuple[str, ...] = field(default_factory=tuple)
|
|
34
|
+
|
|
35
|
+
def __float__(self) -> float:
|
|
36
|
+
raise TypeError(
|
|
37
|
+
"A DADL result is not a bare number: use .raw.value or .corrected.value "
|
|
38
|
+
"explicitly, and keep .labels with it."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
parts = [f"{self.analysis}: raw {self.raw}"]
|
|
43
|
+
if self.corrected is not None:
|
|
44
|
+
parts.append(f"corrected {self.corrected}")
|
|
45
|
+
if self.labels:
|
|
46
|
+
parts.append("[" + "; ".join(self.labels) + "]")
|
|
47
|
+
return " ".join(parts)
|
|
48
|
+
|
|
49
|
+
def as_record(self) -> dict[str, object]:
|
|
50
|
+
"""Flat record for CSV or DataFrame export. Labels are a mandatory column."""
|
|
51
|
+
return {
|
|
52
|
+
"analysis": self.analysis,
|
|
53
|
+
"raw_value": self.raw.value,
|
|
54
|
+
"raw_stderr": self.raw.stderr,
|
|
55
|
+
"corrected_value": None if self.corrected is None else self.corrected.value,
|
|
56
|
+
"corrected_stderr": None if self.corrected is None else self.corrected.stderr,
|
|
57
|
+
"labels": "; ".join(self.labels),
|
|
58
|
+
"artifact_ids": "; ".join(self.artifact_ids),
|
|
59
|
+
}
|
dadl/sim.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Time-tag simulator with known, injected detector imperfections.
|
|
2
|
+
|
|
3
|
+
Every parameter that the calibration engine will later estimate is set
|
|
4
|
+
explicitly here, so tests can compare estimates against the truth.
|
|
5
|
+
|
|
6
|
+
Units: time tags are int64 picoseconds; rates are per second.
|
|
7
|
+
|
|
8
|
+
Phase 1 approximations, to be replaced by the self-exciting detection model
|
|
9
|
+
in docs/design.md:
|
|
10
|
+
- afterpulses are generated for one generation only (no cascades);
|
|
11
|
+
- crosstalk and breakdown flash are not simulated yet.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from dadl.deadtime import apply_nonparalyzable
|
|
22
|
+
|
|
23
|
+
PS_PER_S = 10**12
|
|
24
|
+
|
|
25
|
+
#: Analyzer angles in degrees for the standard CHSH settings on a Phi+ state.
|
|
26
|
+
CHSH_SETTINGS: dict[str, float] = {"a": 0.0, "a'": 45.0, "b": 22.5, "b'": 67.5}
|
|
27
|
+
CHSH_PAIRS: tuple[tuple[str, str], ...] = (("a", "b"), ("a", "b'"), ("a'", "b"), ("a'", "b'"))
|
|
28
|
+
CHANNELS: tuple[str, ...] = ("A+", "A-", "B+", "B-")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class DetectorParams:
|
|
33
|
+
"""Ground truth for one detector channel."""
|
|
34
|
+
|
|
35
|
+
efficiency: float = 0.5 # system efficiency (detector x coupling)
|
|
36
|
+
dark_rate: float = 100.0 # counts per second
|
|
37
|
+
dead_time_ps: int = 22_000 # non-paralyzable
|
|
38
|
+
jitter_ps: float = 350.0 # Gaussian sigma
|
|
39
|
+
afterpulse_prob: float = 0.0 # per recorded avalanche
|
|
40
|
+
afterpulse_tau_ps: float = 50_000.0 # exponential delay after the dead time
|
|
41
|
+
delay_ps: int = 0 # fixed channel delay
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
if not 0.0 <= self.efficiency <= 1.0:
|
|
45
|
+
raise ValueError("efficiency must be in [0, 1]")
|
|
46
|
+
if self.dark_rate < 0 or self.dead_time_ps < 0 or self.jitter_ps < 0:
|
|
47
|
+
raise ValueError("rates, dead time and jitter must be non-negative")
|
|
48
|
+
if not 0.0 <= self.afterpulse_prob < 1.0:
|
|
49
|
+
raise ValueError("afterpulse probability must be in [0, 1)")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class PairSource:
|
|
54
|
+
"""A CW polarization-entangled SPDC source producing a Werner-like Phi+ state."""
|
|
55
|
+
|
|
56
|
+
pair_rate: float = 1e5 # pairs per second at the crystal
|
|
57
|
+
visibility: float = 0.95 # E(alpha, beta) = V cos 2(alpha - beta)
|
|
58
|
+
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
if self.pair_rate < 0:
|
|
61
|
+
raise ValueError("pair rate must be non-negative")
|
|
62
|
+
if not 0.0 <= self.visibility <= 1.0:
|
|
63
|
+
raise ValueError("visibility must be in [0, 1]")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def poisson_times(rate: float, duration_s: float, rng: np.random.Generator) -> np.ndarray:
|
|
67
|
+
"""Sorted float64 arrival times (ps) of a homogeneous Poisson process."""
|
|
68
|
+
n = rng.poisson(rate * duration_s)
|
|
69
|
+
return np.sort(rng.uniform(0.0, duration_s * PS_PER_S, n))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def detect(
|
|
73
|
+
photon_times_ps: np.ndarray,
|
|
74
|
+
det: DetectorParams,
|
|
75
|
+
duration_s: float,
|
|
76
|
+
rng: np.random.Generator,
|
|
77
|
+
) -> np.ndarray:
|
|
78
|
+
"""Turn photon arrival times into recorded int64 time tags for one detector."""
|
|
79
|
+
photons = np.asarray(photon_times_ps, dtype=np.float64)
|
|
80
|
+
kept = photons[rng.random(photons.size) < det.efficiency]
|
|
81
|
+
if det.jitter_ps > 0:
|
|
82
|
+
kept = kept + rng.normal(0.0, det.jitter_ps, kept.size)
|
|
83
|
+
kept = kept + det.delay_ps
|
|
84
|
+
darks = poisson_times(det.dark_rate, duration_s, rng)
|
|
85
|
+
arrivals = np.sort(np.rint(np.concatenate([kept, darks]))).astype(np.int64)
|
|
86
|
+
recorded = apply_nonparalyzable(arrivals, det.dead_time_ps)
|
|
87
|
+
|
|
88
|
+
if det.afterpulse_prob > 0 and recorded.size:
|
|
89
|
+
mask = rng.random(recorded.size) < det.afterpulse_prob
|
|
90
|
+
n_ap = int(mask.sum())
|
|
91
|
+
if n_ap:
|
|
92
|
+
delays = det.dead_time_ps + rng.exponential(det.afterpulse_tau_ps, n_ap)
|
|
93
|
+
afterpulses = np.rint(recorded[mask] + delays).astype(np.int64)
|
|
94
|
+
merged = np.sort(np.concatenate([recorded, afterpulses]))
|
|
95
|
+
recorded = apply_nonparalyzable(merged, det.dead_time_ps)
|
|
96
|
+
return recorded
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def simulate_chsh_setting(
|
|
100
|
+
source: PairSource,
|
|
101
|
+
alpha_deg: float,
|
|
102
|
+
beta_deg: float,
|
|
103
|
+
detectors: dict[str, DetectorParams],
|
|
104
|
+
duration_s: float,
|
|
105
|
+
rng: np.random.Generator,
|
|
106
|
+
) -> dict[str, np.ndarray]:
|
|
107
|
+
"""Recorded tags on channels A+, A-, B+, B- for one pair of analyzer angles."""
|
|
108
|
+
missing = set(CHANNELS) - set(detectors)
|
|
109
|
+
if missing:
|
|
110
|
+
raise ValueError(f"missing detector parameters for {sorted(missing)}")
|
|
111
|
+
t = poisson_times(source.pair_rate, duration_s, rng)
|
|
112
|
+
n = t.size
|
|
113
|
+
a_plus = rng.random(n) < 0.5
|
|
114
|
+
p_same = 0.5 * (1.0 + source.visibility * math.cos(2.0 * math.radians(alpha_deg - beta_deg)))
|
|
115
|
+
same = rng.random(n) < p_same
|
|
116
|
+
b_plus = np.where(same, a_plus, ~a_plus)
|
|
117
|
+
photons = {"A+": t[a_plus], "A-": t[~a_plus], "B+": t[b_plus], "B-": t[~b_plus]}
|
|
118
|
+
return {ch: detect(photons[ch], detectors[ch], duration_s, rng) for ch in CHANNELS}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def simulate_chsh(
|
|
122
|
+
source: PairSource,
|
|
123
|
+
detectors: dict[str, DetectorParams],
|
|
124
|
+
duration_s: float,
|
|
125
|
+
rng: np.random.Generator,
|
|
126
|
+
settings: dict[str, float] | None = None,
|
|
127
|
+
) -> dict[tuple[str, str], dict[str, np.ndarray]]:
|
|
128
|
+
"""One acquisition per CHSH setting pair, keyed like ("a", "b'")."""
|
|
129
|
+
angles = settings or CHSH_SETTINGS
|
|
130
|
+
return {
|
|
131
|
+
(sa, sb): simulate_chsh_setting(source, angles[sa], angles[sb], detectors, duration_s, rng)
|
|
132
|
+
for sa, sb in CHSH_PAIRS
|
|
133
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dadl-quantum
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Detector-Aware Data Layer for quantum optics experiments
|
|
5
|
+
Project-URL: Homepage, https://github.com/usecommon/dadl
|
|
6
|
+
Project-URL: Documentation, https://github.com/usecommon/dadl/blob/main/docs/design.md
|
|
7
|
+
Project-URL: Issues, https://github.com/usecommon/dadl/issues
|
|
8
|
+
License: MIT OR Apache-2.0
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Requires-Dist: pyyaml>=6.0
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# dadl (Python package)
|
|
23
|
+
|
|
24
|
+
Detector-Aware Data Layer for quantum optics experiments.
|
|
25
|
+
|
|
26
|
+
Phase 1 is pure NumPy: a time-tag simulator with injected detector
|
|
27
|
+
imperfections, fixed-grid coincidence counting, CHSH and two-photon
|
|
28
|
+
visibility analyses, the guardrail policy, and the labelled result type.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e ".[test]"
|
|
32
|
+
dadl quickstart chsh-spad
|
|
33
|
+
pytest
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
See the repository README and `docs/design.md` for the full design.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
dadl/__init__.py,sha256=EnugLErrkt54qQdtO_nrBD_GR2A5YkqPOLXxd2E4bDw,433
|
|
2
|
+
dadl/cli.py,sha256=ovXAJjjkRjXLC984Pb4Zvri97wA8XSZgdjOxY9BghFU,2987
|
|
3
|
+
dadl/closed_form.py,sha256=G5j9p9zAG27qigOd0AGCHFjGFfF-9JNXXjB-gI4cJng,2728
|
|
4
|
+
dadl/coincidences.py,sha256=UKSmS3d87QbhLqnO0vmXcUguKRj883LDBaUo-tZP1QA,1094
|
|
5
|
+
dadl/config.py,sha256=pV25pF4R_kXiPBrDd9xJEOKrQN3YaWq3ByRAnYPVLJY,1523
|
|
6
|
+
dadl/deadtime.py,sha256=wbLHBXoYMB2RTvLxXUxa6C2ZaeUhqo3Z5MYjU3_E-cw,1241
|
|
7
|
+
dadl/errors.py,sha256=q7_9FN3LlfkKu9OkdExjhRES06PqvEKlfjTiSA3wV3k,495
|
|
8
|
+
dadl/guardrails.py,sha256=mCaA5P2DP4JUY0hVPDkAr2_i-jSd-YpTbxRAwIpAlnQ,5748
|
|
9
|
+
dadl/result.py,sha256=pqJlfcMaUJsMDFoXwDH5Ic_AyEMn7OgKpV5rj03V6OQ,2000
|
|
10
|
+
dadl/sim.py,sha256=tN4CMeCwQgAzJRsNjNRi1UTVfSnsWTPqyddHzkJjWzw,5225
|
|
11
|
+
dadl/analysis/__init__.py,sha256=wbeH0n_KKHBEfYZsuviZFpqKD7pDx5kYRs8YCwpUh84,230
|
|
12
|
+
dadl/analysis/chsh.py,sha256=CMW7eu2cqlW9sD99qgW7uhEVQ1N85kdt7j83H0SlK0c,2390
|
|
13
|
+
dadl/analysis/visibility.py,sha256=mcvjIGYFMrn0-3Uhml4vbI1cu0ZgNhPnJwMjH9ISEc4,1764
|
|
14
|
+
dadl_quantum-0.0.2.dist-info/METADATA,sha256=7m36g-y50w-CRQ1bDuJ_2HBbs81LR53QJUDqo_bDWkU,1286
|
|
15
|
+
dadl_quantum-0.0.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
16
|
+
dadl_quantum-0.0.2.dist-info/entry_points.txt,sha256=Gl-09w4WNOdJJnnBKjbVGWvzAwVK1iJT5AFyhPrWEJA,39
|
|
17
|
+
dadl_quantum-0.0.2.dist-info/RECORD,,
|