vacspin 0.1.0__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.
vacspin/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ """vacspin: group-IV colour-centre spin-photon interfaces, end to end.
2
+
3
+ From the strained spin Hamiltonian of a vacancy centre in diamond
4
+ (SiV-, GeV-, SnV-, PbV-), through its optical transitions and
5
+ cyclicity, the transition-resolved Purcell budget of a coupled
6
+ cavity, to exact single-shot readout statistics and remote-
7
+ entanglement rates -- with every physics claim in the test suite
8
+ pinned to a closed form, a measured anchor, or two independent code
9
+ paths.
10
+
11
+ Methodological basis: T. M. Mahim, M. M. Rahman and A. S. M. Mohsin,
12
+ "Fast single-shot readout of tin-vacancy spins with an overcoupled
13
+ diamond nanocavity on thin-film lithium niobate" (submitted to Optics
14
+ Express, 2026), and the effective-Hamiltonian and readout formalism of
15
+ Rosenthal et al., PRX 13, 031022 (2023) and arXiv:2403.13110.
16
+ """
17
+ from .params import (EmissionBudget, SpinParameters, siv_hepp2014,
18
+ snv_emission, snv_rosenthal2023)
19
+ from .hamiltonian import (GAMMA_GHZ_PER_T, h_manifold, lab_to_spin,
20
+ qubit_frequency, qubit_frequency_perpendicular,
21
+ solve, zero_field_splitting)
22
+ from .transitions import (cyclicity, field_on_circle, rabi_rate,
23
+ transition_strength, transition_table)
24
+ from .cavity import (CavityInterface, lorentzian_suppression,
25
+ purcell_max, xi_pol_overlap)
26
+ from .readout import (fidelity, fidelity_threshold1, geometric_pmf,
27
+ poisson_pmf, polarization_rate, readout_counts,
28
+ required_efficiency, required_window)
29
+ from .remote import barrett_kok_success, entanglement_rate
30
+
31
+ __version__ = "0.1.0"
32
+ __all__ = [
33
+ "SpinParameters", "EmissionBudget", "snv_rosenthal2023",
34
+ "snv_emission", "siv_hepp2014",
35
+ "GAMMA_GHZ_PER_T", "h_manifold", "lab_to_spin", "solve",
36
+ "zero_field_splitting", "qubit_frequency",
37
+ "qubit_frequency_perpendicular",
38
+ "transition_strength", "transition_table", "cyclicity", "rabi_rate",
39
+ "field_on_circle",
40
+ "purcell_max", "lorentzian_suppression", "xi_pol_overlap",
41
+ "CavityInterface",
42
+ "polarization_rate", "readout_counts", "fidelity_threshold1",
43
+ "geometric_pmf", "poisson_pmf", "fidelity", "required_efficiency",
44
+ "required_window",
45
+ "barrett_kok_success", "entanglement_rate",
46
+ "__version__",
47
+ ]
vacspin/cavity.py ADDED
@@ -0,0 +1,174 @@
1
+ """The cavity-coupled interface: transition-resolved Purcell budget.
2
+
3
+ Everything between the spin and the detector, in the bad-cavity
4
+ (Purcell) regime, following the transition-resolved conventions of
5
+ Lee et al., arXiv:2511.05740 (Eqs. 1-10) as used by Mahim et al.
6
+ (Optics Express, 2026) for the SnV-on-TFLN interface:
7
+
8
+ F_max = (3 / 4 pi^2) (lam/n)^3 Q_L / V ideal Purcell factor
9
+ F_C = 1 + (F_max - 1) xi_pol xi_pos the coupled transition
10
+ F_C2 = same, suppressed by the cavity Lorentzian at the qubit
11
+ detuning (the spin-flipping partner)
12
+ zeta = 1 + eta_q eta_DW eta_BR (F_C - 1) lifetime reduction
13
+ beta = eta_q eta_DW eta_BR F_C / zeta photons into the mode
14
+ eta = beta eta_wg T_link eta_chip eta_det detected per decay
15
+ Lambda_cav = Lambda_0 zeta / zeta_flip cavity-boosted cyclicity
16
+ g = (1/2) sqrt(F_C kappa gamma_rad,C) coupling rate, so that
17
+ F_C = 4 g^2 / (kappa gamma_rad,C) holds as an identity
18
+
19
+ with V in units of (lam/n)^3 and kappa = f_cav / Q_L. `xi_pol_overlap`
20
+ gives the polarisation overlap sin^2(theta) (1 + sin 2 psi)/2 of a
21
+ <111> dipole with a linearly polarised cavity mode (2/3 at psi = 45 deg
22
+ for <100>-oriented diamond).
23
+
24
+ Validity is physics, not paperwork, so it is computed rather than
25
+ assumed: the `validity` dict carries the bad-cavity, weak-coupling and
26
+ spin-selectivity conditions, and `require_valid()` refuses -- with the
27
+ failing conditions named -- instead of returning numbers whose
28
+ derivation no longer applies.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import numpy as np
33
+
34
+ from .params import EmissionBudget
35
+
36
+ __all__ = ["purcell_max", "lorentzian_suppression", "xi_pol_overlap",
37
+ "CavityInterface"]
38
+
39
+
40
+ def purcell_max(q_loaded, v_rel):
41
+ """Ideal Purcell factor F = (3/4 pi^2) Q/V, V in (lam/n)^3."""
42
+ q, v = float(q_loaded), float(v_rel)
43
+ if q <= 0 or v <= 0:
44
+ raise ValueError("Q and V must be positive")
45
+ return 3.0 / (4.0 * np.pi ** 2) * q / v
46
+
47
+
48
+ def lorentzian_suppression(delta_hz, f_cav_hz, q_loaded):
49
+ """Purcell reduction of a transition detuned by delta from the
50
+ cavity: L = 1 / (1 + (2 Q delta / f_cav)^2). Exactly 1 on
51
+ resonance and exactly 1/2 at delta = f_cav / (2 Q) = kappa/2."""
52
+ if f_cav_hz <= 0 or q_loaded <= 0:
53
+ raise ValueError("f_cav_hz and Q must be positive")
54
+ x = 2.0 * float(q_loaded) * float(delta_hz) / float(f_cav_hz)
55
+ return 1.0 / (1.0 + x * x)
56
+
57
+
58
+ def xi_pol_overlap(theta_deg=54.7356, psi_deg=45.0):
59
+ """Polarisation overlap |mu_hat . e_cav|^2 of a <111> dipole with a
60
+ linearly polarised in-plane cavity mode:
61
+ sin^2(theta) (1 + sin 2 psi) / 2, equal to 2/3 at psi = 45 deg for
62
+ theta = arccos(1/sqrt 3) (a <100>-oriented sample)."""
63
+ th = np.deg2rad(float(theta_deg))
64
+ ps = np.deg2rad(float(psi_deg))
65
+ return float(np.sin(th) ** 2 * (1.0 + np.sin(2.0 * ps)) / 2.0)
66
+
67
+
68
+ class CavityInterface:
69
+ """All derived quantities of one cavity-coupled colour centre.
70
+
71
+ Parameters
72
+ ----------
73
+ q_loaded : loaded quality factor of the cavity.
74
+ v_rel : mode volume in units of (lam/n)^3.
75
+ eta_wg : fraction of cavity decay leaving through the collection
76
+ port (kappa_wg / kappa_total), in [0, 1].
77
+ budget : `EmissionBudget` of the emitter (cited).
78
+ lambda0 : bare (no-cavity) cyclicity of the operating point --
79
+ compute it with `vacspin.cyclicity` from your spin parameters
80
+ and field, or measure it.
81
+ omega_q_hz : qubit frequency (Hz), the detuning of the
82
+ spin-flipping partner transition.
83
+ xi_pol, xi_pos : polarisation and position overlap of the emitter
84
+ with the cavity field, each in [0, 1].
85
+ t_link, eta_chip, eta_det : downstream transfer, routing and
86
+ detector efficiencies, each in [0, 1] (defaults 1: the
87
+ cavity-only budget).
88
+ delta_partner_hz : optional detuning of a competing SAME-spin
89
+ transition (e.g. the D line); when given, its Purcell factor
90
+ F_partner and the selectivity condition kappa < delta are
91
+ reported.
92
+
93
+ Everything is computed in __init__; `validity` holds the regime
94
+ checks and `require_valid()` refuses when any fails.
95
+ """
96
+
97
+ def __init__(self, q_loaded, v_rel, eta_wg, budget: EmissionBudget,
98
+ lambda0, omega_q_hz, xi_pol, xi_pos=1.0,
99
+ t_link=1.0, eta_chip=1.0, eta_det=1.0,
100
+ delta_partner_hz=None):
101
+ for name, v in (("eta_wg", eta_wg), ("xi_pol", xi_pol),
102
+ ("xi_pos", xi_pos), ("t_link", t_link),
103
+ ("eta_chip", eta_chip), ("eta_det", eta_det)):
104
+ if not (0.0 <= float(v) <= 1.0):
105
+ raise ValueError(f"{name} must be in [0, 1]")
106
+ if lambda0 <= 0 or not np.isfinite(lambda0):
107
+ raise ValueError("lambda0 must be a finite positive "
108
+ "cyclicity (compute or measure it)")
109
+ if omega_q_hz <= 0:
110
+ raise ValueError("omega_q_hz must be positive")
111
+ self.q = float(q_loaded)
112
+ self.v = float(v_rel)
113
+ self.budget = budget
114
+ self.f_cav_hz = 299792458.0 / (budget.zpl_nm * 1e-9)
115
+ self.kappa_hz = self.f_cav_hz / self.q
116
+ self.omega_q_hz = float(omega_q_hz)
117
+
118
+ a = budget.radiative_fraction
119
+ self.f_max = purcell_max(q_loaded, v_rel)
120
+ self.f_c = 1.0 + (self.f_max - 1.0) * xi_pol * xi_pos
121
+ lc2 = lorentzian_suppression(omega_q_hz, self.f_cav_hz, self.q)
122
+ self.f_c2 = 1.0 + (self.f_max - 1.0) * xi_pol * xi_pos * lc2
123
+ self.zeta = 1.0 + a * (self.f_c - 1.0)
124
+ zeta_flip = 1.0 + a * (self.f_c2 - 1.0)
125
+ self.gamma_cav = budget.gamma0 * self.zeta # 1/s
126
+ self.tau_cav_s = 1.0 / self.gamma_cav
127
+ self.lambda_cav = float(lambda0) * self.zeta / zeta_flip
128
+ self.beta = a * self.f_c / self.zeta
129
+ self.eta_wg = float(eta_wg)
130
+ self.eta = (self.beta * self.eta_wg * float(t_link)
131
+ * float(eta_chip) * float(eta_det))
132
+ # coupling rate defined so F_C = 4 g^2 / (kappa gamma_rad,C)
133
+ gamma_rad_c = budget.gamma0 * a # 1/s
134
+ self.g_hz = 0.5 * np.sqrt(self.f_c * self.kappa_hz * gamma_rad_c)
135
+ self.cooperativity = (4.0 * self.g_hz ** 2
136
+ / (self.kappa_hz * budget.gamma0))
137
+ gamma_cav_hz = self.gamma_cav / (2.0 * np.pi)
138
+ self.validity = dict(
139
+ spin_selectivity=bool(gamma_cav_hz < omega_q_hz / 5.0),
140
+ bad_cavity=bool(gamma_cav_hz < self.kappa_hz / 10.0),
141
+ weak_coupling=bool(self.g_hz < self.kappa_hz / 10.0),
142
+ )
143
+ self.f_partner = None
144
+ if delta_partner_hz is not None:
145
+ self.f_partner = 1.0 + (self.f_max - 1.0) * xi_pol * xi_pos \
146
+ * lorentzian_suppression(delta_partner_hz,
147
+ self.f_cav_hz, self.q)
148
+ self.validity["partner_selectivity"] = \
149
+ bool(self.kappa_hz < float(delta_partner_hz))
150
+
151
+ def require_valid(self):
152
+ """Refuse -- naming the failed conditions -- unless every
153
+ regime assumption behind the formulas above holds."""
154
+ bad = [k for k, ok in self.validity.items() if not ok]
155
+ if bad:
156
+ raise ValueError(
157
+ "interface outside its regime of validity: "
158
+ f"{', '.join(bad)} failed. The Purcell/rate formulas "
159
+ "assume the bad-cavity, weak-coupling, spin-selective "
160
+ "regime; change Q_L, V or the operating point.")
161
+ return self
162
+
163
+ def summary(self):
164
+ out = dict(q_loaded=self.q, v_rel=self.v, f_max=self.f_max,
165
+ f_c=self.f_c, f_c2=self.f_c2, zeta=self.zeta,
166
+ tau_cav_ns=self.tau_cav_s * 1e9,
167
+ lambda_cav=self.lambda_cav, beta=self.beta,
168
+ eta=self.eta, g_mhz=self.g_hz / 1e6,
169
+ kappa_ghz=self.kappa_hz / 1e9,
170
+ cooperativity=self.cooperativity,
171
+ validity=dict(self.validity))
172
+ if self.f_partner is not None:
173
+ out["f_partner"] = self.f_partner
174
+ return out
vacspin/hamiltonian.py ADDED
@@ -0,0 +1,132 @@
1
+ """The group-IV effective Hamiltonian: 4x4 per manifold, pure NumPy.
2
+
3
+ Implements the effective ground/excited-manifold Hamiltonian shared by
4
+ all group-IV vacancy centres in diamond, exactly as written for the
5
+ SnV- in Rosenthal et al., Phys. Rev. X 13, 031022 (2023), Appendix B
6
+ (Eqs. B1-B5), in the basis {|ex up>, |ex dn>, |ey up>, |ey dn>}
7
+ (orbital doublet x spin 1/2):
8
+
9
+ H = H_SO + H_JT + H_Z + H_L
10
+
11
+ H_SO = -(lam/2) [[0, i], [-i, 0]] (x) sigma_z (spin-orbit)
12
+ H_JT = [[Ux, Uy], [Uy, -Ux]] (x) 1 (strain/JT)
13
+ H_Z = (gamma/2) 1 (x) [[(1+2 delta) Bz, Bx - i By],
14
+ [Bx + i By, -(1+2 delta) Bz]] (spin Zeeman)
15
+ H_L = (gamma f/2) Bz [[0, i], [-i, 0]] (x) 1 (orbital Zeeman)
16
+
17
+ with all energies in GHz (angular frequency over 2 pi), fields in
18
+ tesla, and gamma/2pi = 28.0 GHz/T the electron gyromagnetic ratio. The
19
+ field enters in the SPIN FRAME whose z axis lies along the centre's
20
+ <111> high-symmetry axis; `lab_to_spin` performs the exact rotation
21
+ from your lab frame using the (theta, phi) of the parameter set.
22
+
23
+ Exact structure this module's tests lean on rather than stored
24
+ numbers: the Hamiltonian is Hermitian for every input; at zero field
25
+ each manifold consists of two exact Kramers doublets split by the
26
+ closed form Delta = sqrt(lam^2 + 4 (Ux^2 + Uy^2)); the perturbative
27
+ qubit frequency for a purely transverse field (Rosenthal Eq. B9),
28
+ f = 2 gamma B_perp Ups / Delta, agrees with full diagonalisation; and
29
+ the frame rotation is exactly orthonormal, so |B| is preserved to
30
+ machine precision.
31
+ """
32
+ from __future__ import annotations
33
+
34
+ import numpy as np
35
+
36
+ from .params import SpinParameters
37
+
38
+ __all__ = ["GAMMA_GHZ_PER_T", "h_manifold", "lab_to_spin", "solve",
39
+ "zero_field_splitting", "qubit_frequency",
40
+ "qubit_frequency_perpendicular"]
41
+
42
+ # electron gyromagnetic ratio gamma/2pi (GHz/T), gamma = g_s mu_B / hbar
43
+ # with g_s ~ 2: the standard value used by Rosenthal et al. (2023).
44
+ GAMMA_GHZ_PER_T = 28.0
45
+
46
+ _I2 = np.eye(2)
47
+ _SZ = np.array([[1.0, 0.0], [0.0, -1.0]])
48
+ _ORB_Y = np.array([[0.0, 1j], [-1j, 0.0]]) # orbital angular momentum
49
+
50
+
51
+ def h_manifold(lam, ups_x, ups_y, f, delta, b_spin):
52
+ """4x4 manifold Hamiltonian (GHz) for a field b_spin = (Bx, By, Bz)
53
+ in tesla, given in the spin frame."""
54
+ bx, by, bz = (float(b) for b in b_spin)
55
+ h_so = -0.5 * lam * np.kron(_ORB_Y, _SZ)
56
+ h_jt = np.kron(np.array([[ups_x, ups_y], [ups_y, -ups_x]]), _I2)
57
+ zee = np.array([[(1 + 2 * delta) * bz, bx - 1j * by],
58
+ [bx + 1j * by, -(1 + 2 * delta) * bz]])
59
+ h_z = 0.5 * GAMMA_GHZ_PER_T * np.kron(_I2, zee)
60
+ h_l = 0.5 * GAMMA_GHZ_PER_T * f * bz * np.kron(_ORB_Y, _I2)
61
+ return h_so + h_jt + h_z + h_l
62
+
63
+
64
+ def lab_to_spin(v_lab, theta_rad, phi_rad):
65
+ """Rotate a lab-frame vector into the spin frame whose z axis
66
+ points along the centre's <111> axis at polar angle theta, azimuth
67
+ phi. Rows of the rotation are the spin-frame basis vectors in the
68
+ lab frame; the rotation is exactly orthonormal."""
69
+ ct, st = np.cos(theta_rad), np.sin(theta_rad)
70
+ cp, sp = np.cos(phi_rad), np.sin(phi_rad)
71
+ ex = np.array([ct * cp, ct * sp, -st])
72
+ ey = np.array([-sp, cp, 0.0])
73
+ ez = np.array([st * cp, st * sp, ct])
74
+ return np.vstack([ex, ey, ez]) @ np.asarray(v_lab, dtype=float)
75
+
76
+
77
+ def zero_field_splitting(lam, ups_x, ups_y=0.0):
78
+ """Exact zero-field splitting of one manifold:
79
+ Delta = sqrt(lam^2 + 4 (Ux^2 + Uy^2))."""
80
+ return float(np.sqrt(lam ** 2 + 4.0 * (ups_x ** 2 + ups_y ** 2)))
81
+
82
+
83
+ def solve(params: SpinParameters, b_lab, strain_scale=1.0):
84
+ """Eigenstructure of both manifolds at a lab-frame field (tesla).
85
+
86
+ strain_scale multiplies the strain of both manifolds (the strain-
87
+ engineering axis); the strain axis is taken along x, which fixes
88
+ the azimuthal origin of the spin frame.
89
+
90
+ Returns (evals_g, evecs_g, evals_e, evecs_e): energies in GHz
91
+ sorted ascending, eigenvectors as columns.
92
+ """
93
+ if not np.all(np.isfinite(b_lab)):
94
+ raise ValueError("magnetic field must be finite")
95
+ s = float(strain_scale)
96
+ if s < 0:
97
+ raise ValueError("strain_scale must be >= 0")
98
+ bs = lab_to_spin(b_lab, params.theta_rad, params.phi_rad)
99
+ hg = h_manifold(params.lam_g, s * params.ups_g, 0.0,
100
+ params.f_g, params.delta_g, bs)
101
+ he = h_manifold(params.lam_e, s * params.ups_e, 0.0,
102
+ params.f_e, params.delta_e, bs)
103
+ eg, vg = np.linalg.eigh(hg)
104
+ ee, ve = np.linalg.eigh(he)
105
+ return eg, vg, ee, ve
106
+
107
+
108
+ def qubit_frequency(params: SpinParameters, b_lab, strain_scale=1.0):
109
+ """Ground-state qubit frequency (GHz): the splitting of the lowest
110
+ Kramers doublet at the given lab-frame field."""
111
+ eg, _, _, _ = solve(params, b_lab, strain_scale)
112
+ return float(eg[1] - eg[0])
113
+
114
+
115
+ def qubit_frequency_perpendicular(params: SpinParameters, b_perp_t,
116
+ manifold="ground"):
117
+ """Perturbative qubit frequency for a purely TRANSVERSE field
118
+ (Rosenthal et al. Eq. B9): f = 2 gamma B_perp Ups / Delta, valid
119
+ for gamma B_perp << Delta. The tests hold this against full
120
+ diagonalisation; use `qubit_frequency` for anything quantitative.
121
+ """
122
+ b = float(b_perp_t)
123
+ if b < 0:
124
+ raise ValueError("b_perp_t must be >= 0")
125
+ if manifold == "ground":
126
+ lam, ups = params.lam_g, params.ups_g
127
+ elif manifold == "excited":
128
+ lam, ups = params.lam_e, params.ups_e
129
+ else:
130
+ raise ValueError('manifold must be "ground" or "excited"')
131
+ delta = zero_field_splitting(lam, ups)
132
+ return 2.0 * GAMMA_GHZ_PER_T * b * ups / delta
vacspin/params.py ADDED
@@ -0,0 +1,188 @@
1
+ """Cited parameter sets for group-IV colour-centre spin-photon interfaces.
2
+
3
+ No physical number in this package is made up, and none is accepted
4
+ without a source: both dataclasses refuse to exist without a
5
+ `reference`. Two sets ship, and each ships only what its source states:
6
+
7
+ * `snv_rosenthal2023` / `snv_emission`: the negatively charged
8
+ tin-vacancy (SnV-) centre in diamond, the device-fit values of
9
+ Table I of J. A. Rosenthal et al., Phys. Rev. X 13, 031022 (2023)
10
+ (arXiv:2306.13199), with the ab initio orbital-quenching reduction
11
+ factors of G. Thiering and A. Gali, Phys. Rev. X 8, 021063 (2018),
12
+ and the emission budget of Rosenthal et al., arXiv:2403.13110
13
+ (lifetime, quantum efficiency) with the Debye-Waller factor of
14
+ Goerlitz et al. and the C/D branching ratio of Lee et al.,
15
+ arXiv:2511.05740. This set is validated against measured numbers in
16
+ the test suite (zero-field splittings, qubit frequency, cyclicity,
17
+ Rabi rate) with no free parameters.
18
+ * `siv_hepp2014`: the silicon-vacancy (SiV-) centre, unstrained
19
+ spin-orbit splittings of C. Hepp et al., Phys. Rev. Lett. 112,
20
+ 036405 (2014) (arXiv:1310.3106): 50 GHz ground, 260 GHz excited,
21
+ 737 nm zero-phonon line. Strain and orbital quenching are
22
+ sample-specific and deliberately NOT shipped for SiV: the set
23
+ carries zero strain and zero quenching, stated in its reference
24
+ string, and you supply your measured values for field-dependent
25
+ work.
26
+
27
+ For any other centre (GeV-, PbV-, or your own SnV sample), populate the
28
+ dataclasses from your measurements or the literature; the mandatory
29
+ `reference` field keeps the provenance attached to every prediction.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import dataclasses
34
+
35
+ import numpy as np
36
+
37
+ __all__ = ["SpinParameters", "EmissionBudget", "snv_rosenthal2023",
38
+ "snv_emission", "siv_hepp2014"]
39
+
40
+
41
+ def _check_ref(reference):
42
+ if not isinstance(reference, str) or len(reference.strip()) < 8:
43
+ raise ValueError("a real `reference` string is required: every "
44
+ "physical number in this package carries its "
45
+ "source")
46
+
47
+
48
+ @dataclasses.dataclass(frozen=True)
49
+ class SpinParameters:
50
+ """Effective-Hamiltonian parameters of one group-IV centre.
51
+
52
+ All group-IV vacancy centres (SiV-, GeV-, SnV-, PbV-) share the
53
+ same D3d level structure: an orbital doublet times spin 1/2 in
54
+ each manifold, split by spin-orbit coupling and strain. Units:
55
+ every energy in GHz (angular frequency over 2 pi), fields in
56
+ tesla.
57
+
58
+ lam_g, lam_e : spin-orbit constants of ground/excited manifold.
59
+ ups_g, ups_e : Jahn-Teller/strain energies (the x component; the
60
+ strain axis fixes the azimuthal origin of the spin frame).
61
+ f_g, f_e : orbital Zeeman quenching factors (gL * p).
62
+ delta_g, delta_e : spin g-factor anisotropies (gL * delta_p).
63
+ theta_rad, phi_rad : polar and azimuthal angle of the centre's
64
+ high-symmetry <111> axis in your lab frame.
65
+ reference : where every number comes from. Required, on purpose.
66
+ """
67
+
68
+ lam_g: float
69
+ ups_g: float
70
+ lam_e: float
71
+ ups_e: float
72
+ f_g: float
73
+ f_e: float
74
+ delta_g: float
75
+ delta_e: float
76
+ theta_rad: float
77
+ phi_rad: float
78
+ reference: str
79
+
80
+ def __post_init__(self):
81
+ _check_ref(self.reference)
82
+ for name in ("lam_g", "lam_e"):
83
+ if getattr(self, name) <= 0:
84
+ raise ValueError(f"{name} must be positive (GHz)")
85
+ for name in ("ups_g", "ups_e"):
86
+ if getattr(self, name) < 0:
87
+ raise ValueError(f"{name} must be >= 0 (GHz); the strain "
88
+ "axis convention absorbs the sign")
89
+
90
+
91
+ @dataclasses.dataclass(frozen=True)
92
+ class EmissionBudget:
93
+ """Optical emission budget of one centre (bulk values).
94
+
95
+ tau0_s : total excited-state lifetime (s).
96
+ eta_q : radiative quantum efficiency in [0, 1].
97
+ eta_dw : Debye-Waller (zero-phonon) fraction in [0, 1].
98
+ eta_br : branching ratio into the cavity-coupled transition, [0, 1].
99
+ zpl_nm : zero-phonon-line wavelength (nm).
100
+ reference : where the numbers come from. Required.
101
+ """
102
+
103
+ tau0_s: float
104
+ eta_q: float
105
+ eta_dw: float
106
+ eta_br: float
107
+ zpl_nm: float
108
+ reference: str
109
+
110
+ def __post_init__(self):
111
+ _check_ref(self.reference)
112
+ if not (self.tau0_s > 0 and np.isfinite(self.tau0_s)):
113
+ raise ValueError("tau0_s must be a positive lifetime in s")
114
+ if not (self.zpl_nm > 0 and np.isfinite(self.zpl_nm)):
115
+ raise ValueError("zpl_nm must be a positive wavelength in nm")
116
+ for name in ("eta_q", "eta_dw", "eta_br"):
117
+ v = getattr(self, name)
118
+ if not (0.0 < v <= 1.0):
119
+ raise ValueError(f"{name} must be in (0, 1]")
120
+
121
+ @property
122
+ def gamma0(self):
123
+ """Total decay rate 1/tau0 (1/s)."""
124
+ return 1.0 / self.tau0_s
125
+
126
+ @property
127
+ def radiative_fraction(self):
128
+ """eta_q * eta_dw * eta_br: the fraction of decays that emit a
129
+ zero-phonon photon on the cavity-coupled transition."""
130
+ return self.eta_q * self.eta_dw * self.eta_br
131
+
132
+
133
+ def snv_rosenthal2023() -> SpinParameters:
134
+ """SnV- device values, Table I of Rosenthal et al., Phys. Rev. X
135
+ 13, 031022 (2023), with quenching reduction factors from Thiering
136
+ and Gali, Phys. Rev. X 8, 021063 (2018):
137
+ f = gL p, delta = gL delta_p. The dipole angles are the same
138
+ paper's device fit (theta fixed at 125.3 deg, phi fitted 37.33
139
+ deg). Validated against measured splittings, qubit frequency,
140
+ cyclicity and Rabi rate in this package's test suite."""
141
+ gL_g, gL_e = 0.363, 0.581
142
+ p_g, p_e = 0.471, 0.125
143
+ dp_g, dp_e = 0.042, 0.303
144
+ return SpinParameters(
145
+ lam_g=830.15, ups_g=177.67, lam_e=2988.0, ups_e=134.00,
146
+ f_g=gL_g * p_g, f_e=gL_e * p_e,
147
+ delta_g=gL_g * dp_g, delta_e=gL_e * dp_e,
148
+ theta_rad=np.deg2rad(125.3), phi_rad=np.deg2rad(37.33),
149
+ reference="Rosenthal et al., PRX 13, 031022 (2023), Table I "
150
+ "(device values); quenching: Thiering & Gali, PRX 8, "
151
+ "021063 (2018)")
152
+
153
+
154
+ def snv_emission() -> EmissionBudget:
155
+ """SnV- emission budget: lifetime 4.5 ns and quantum efficiency
156
+ 0.8 from Rosenthal et al., arXiv:2403.13110 (Table II);
157
+ Debye-Waller factor 0.57 (Goerlitz et al. 2020, as used by Lee et
158
+ al.); C/D branching ratio 0.75 (Lee et al., arXiv:2511.05740);
159
+ zero-phonon line 619 nm."""
160
+ return EmissionBudget(
161
+ tau0_s=4.5e-9, eta_q=0.80, eta_dw=0.57, eta_br=0.75,
162
+ zpl_nm=619.0,
163
+ reference="Rosenthal et al., arXiv:2403.13110 (tau, eta_q); "
164
+ "Goerlitz et al. 2020 (eta_DW); Lee et al., "
165
+ "arXiv:2511.05740 (eta_BR)")
166
+
167
+
168
+ def siv_hepp2014(theta_rad=None, phi_rad=None) -> SpinParameters:
169
+ """SiV- unstrained model values of Hepp et al., PRL 112, 036405
170
+ (2014): spin-orbit splittings 50 GHz (ground) and 260 GHz
171
+ (excited) at zero strain. Strain and orbital quenching are
172
+ sample-specific and NOT shipped: this set carries ups = 0 and
173
+ f = delta = 0, stated here on purpose -- supply your measured
174
+ values for quantitative field-dependent work. Default orientation:
175
+ the <111> axis of a (001)-oriented sample (theta = arccos(1/sqrt 3),
176
+ phi = 45 deg), exact crystallography, overridable."""
177
+ if theta_rad is None:
178
+ theta_rad = float(np.arccos(1.0 / np.sqrt(3.0)))
179
+ if phi_rad is None:
180
+ phi_rad = float(np.pi / 4)
181
+ return SpinParameters(
182
+ lam_g=50.0, ups_g=0.0, lam_e=260.0, ups_e=0.0,
183
+ f_g=0.0, f_e=0.0, delta_g=0.0, delta_e=0.0,
184
+ theta_rad=theta_rad, phi_rad=phi_rad,
185
+ reference="Hepp et al., PRL 112, 036405 (2014): unstrained "
186
+ "splittings 50/260 GHz, ZPL 737 nm; strain and "
187
+ "orbital quenching deliberately not shipped (sample-"
188
+ "specific) -- supply measured values")