Skip to content

Commit d0aa6ba

Browse files
Add NLR noise model (#398)
1 parent cf4a153 commit d0aa6ba

File tree

7 files changed

+352
-128
lines changed

7 files changed

+352
-128
lines changed

surface_sim/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
IncResMeasNoiseModel,
77
MeasurementNoiseModel,
88
MovableQubitsCircuitNoiseModel,
9+
NLRNoiseModel,
910
NoiselessModel,
1011
PhenomenologicalDepolNoiseModel,
1112
PhenomenologicalNoiseModel,
@@ -28,6 +29,7 @@
2829
"IncResMeasNoiseModel",
2930
"MeasurementNoiseModel",
3031
"SI1000NoiseModel",
32+
"NLRNoiseModel",
3133
"MovableQubitsCircuitNoiseModel",
3234
"SD6NoiseModel",
3335
]

surface_sim/models/library.py

Lines changed: 170 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import annotations
22

3-
from collections.abc import Collection
3+
import math
4+
from collections.abc import Collection, Sequence
45

56
from stim import Circuit, CircuitInstruction
67
from typing_extensions import override
78

89
from ..layouts import Layout
910
from ..setups import (
11+
NLR,
1012
SD6,
1113
SI1000,
1214
BiasedCircuitNoiseSetup,
@@ -17,12 +19,19 @@
1719
PhenomenologicalNoiseSetup,
1820
Setup,
1921
)
20-
from ..setups.setup import SQ_GATES, SQ_MEASUREMENTS, SQ_RESETS, TQ_GATES
22+
from ..setups.setup import (
23+
LONG_RANGE_TQ_GATES,
24+
SQ_GATES,
25+
SQ_MEASUREMENTS,
26+
SQ_RESETS,
27+
TQ_GATES,
28+
)
2129
from .model import Model
2230
from .util import biased_prefactors, grouper, idle_error_probs
2331

24-
NOT_MEAS = SQ_GATES | TQ_GATES | SQ_RESETS
25-
ALL_OPS = SQ_GATES | TQ_GATES | SQ_MEASUREMENTS | SQ_RESETS
32+
NOT_MEAS = SQ_GATES | TQ_GATES | LONG_RANGE_TQ_GATES | SQ_RESETS
33+
ALL_TQ_GATES = TQ_GATES | LONG_RANGE_TQ_GATES
34+
ALL_OPS = SQ_GATES | ALL_TQ_GATES | SQ_MEASUREMENTS | SQ_RESETS
2635

2736

2837
class CircuitNoiseModel(Model):
@@ -31,112 +40,109 @@ class CircuitNoiseModel(Model):
3140
@override
3241
def __getattribute__(self, name: str) -> object:
3342
attr = super().__getattribute__(name)
34-
3543
if not callable(attr):
3644
return attr
3745

3846
if name in SQ_GATES:
47+
return self._sq_gate_generator(name)
48+
elif name in ALL_TQ_GATES:
49+
return self._tq_gate_generator(name)
50+
elif name in SQ_MEASUREMENTS:
51+
return self._sq_meas_generator(name)
52+
elif name in SQ_RESETS:
53+
return self._sq_reset_generator(name)
54+
return attr
3955

40-
def sq_gate(qubits: Collection[str]) -> Circuit:
41-
inds = self.get_inds(qubits)
42-
circ = Circuit()
43-
44-
circ.append(CircuitInstruction(SQ_GATES[name], inds))
45-
if self.uniform:
46-
prob: float = self.param(f"{name}_error_prob")
47-
circ.append(CircuitInstruction("DEPOLARIZE1", inds, [prob]))
48-
else:
49-
for qubit, ind in zip(qubits, inds):
50-
prob: float = self.param(f"{name}_error_prob", qubit)
51-
circ.append(CircuitInstruction("DEPOLARIZE1", [ind], [prob]))
52-
return circ
53-
54-
return sq_gate
56+
def _sq_gate_generator(self, name: str):
57+
def sq_gate(qubits: Collection[str]) -> Circuit:
58+
inds = self.get_inds(qubits)
59+
circ = Circuit()
5560

56-
elif name in TQ_GATES:
61+
circ.append(CircuitInstruction(SQ_GATES[name], inds))
62+
if self.uniform:
63+
prob: float = self.param(f"{name}_error_prob")
64+
circ.append(CircuitInstruction("DEPOLARIZE1", inds, [prob]))
65+
else:
66+
for qubit, ind in zip(qubits, inds):
67+
prob: float = self.param(f"{name}_error_prob", qubit)
68+
circ.append(CircuitInstruction("DEPOLARIZE1", [ind], [prob]))
69+
return circ
5770

58-
def tq_gate(qubits: Collection[str]) -> Circuit:
59-
if len(qubits) % 2 != 0:
60-
raise ValueError("Expected and even number of qubits.")
71+
return sq_gate
6172

62-
inds = self.get_inds(qubits)
63-
circ = Circuit()
73+
def _tq_gate_generator(self, name: str):
74+
def tq_gate(qubits: Collection[str]) -> Circuit:
75+
if len(qubits) % 2 != 0:
76+
raise ValueError("Expected and even number of qubits.")
6477

65-
circ.append(CircuitInstruction(TQ_GATES[name], inds))
66-
if self.uniform:
67-
prob: float = self.param(f"{name}_error_prob")
68-
circ.append(CircuitInstruction("DEPOLARIZE2", inds, [prob]))
69-
else:
70-
for qubit_pair, ind_pair in zip(
71-
grouper(qubits, 2), grouper(inds, 2)
72-
):
73-
prob: float = self.param(f"{name}_error_prob", qubit_pair)
74-
circ.append(CircuitInstruction("DEPOLARIZE2", ind_pair, [prob]))
75-
return circ
78+
inds = self.get_inds(qubits)
79+
circ = Circuit()
7680

77-
return tq_gate
81+
circ.append(CircuitInstruction(ALL_TQ_GATES[name], inds))
82+
if self.uniform:
83+
prob: float = self.param(f"{name}_error_prob")
84+
circ.append(CircuitInstruction("DEPOLARIZE2", inds, [prob]))
85+
else:
86+
for qubit_pair, ind_pair in zip(grouper(qubits, 2), grouper(inds, 2)):
87+
prob: float = self.param(f"{name}_error_prob", qubit_pair)
88+
circ.append(CircuitInstruction("DEPOLARIZE2", ind_pair, [prob]))
89+
return circ
7890

79-
elif name in SQ_MEASUREMENTS:
91+
return tq_gate
8092

81-
def sq_meas(qubits: Collection[str]) -> Circuit:
82-
inds = self.get_inds(qubits)
83-
noise_name = "X_ERROR" if "_x" not in name else "Z_ERROR"
84-
circ = Circuit()
93+
def _sq_meas_generator(self, name: str):
94+
def sq_meas(qubits: Collection[str]) -> Circuit:
95+
inds = self.get_inds(qubits)
96+
noise_name = "X_ERROR" if "_x" not in name else "Z_ERROR"
97+
circ = Circuit()
8598

86-
# separates X_ERROR and MZ lines for clearer stim.Circuits and diagrams
87-
if self.uniform:
88-
prob: float = self.param(f"{name}_error_prob")
89-
circ.append(CircuitInstruction(noise_name, inds, [prob]))
90-
for qubit in qubits:
91-
self.add_meas(qubit)
92-
if self.param("assign_error_flag"):
93-
prob: float = self.param("assign_error_prob")
99+
# separates X_ERROR and MZ lines for clearer stim.Circuits and diagrams
100+
if self.uniform:
101+
prob: float = self.param(f"{name}_error_prob")
102+
circ.append(CircuitInstruction(noise_name, inds, [prob]))
103+
for qubit in qubits:
104+
self.add_meas(qubit)
105+
if self.param("assign_error_flag"):
106+
prob: float = self.param("assign_error_prob")
107+
circ.append(CircuitInstruction(SQ_MEASUREMENTS[name], inds, [prob]))
108+
else:
109+
circ.append(CircuitInstruction(SQ_MEASUREMENTS[name], inds))
110+
else:
111+
for qubit, ind in zip(qubits, inds):
112+
prob: float = self.param(f"{name}_error_prob", qubit)
113+
circ.append(CircuitInstruction(noise_name, [ind], [prob]))
114+
for qubit, ind in zip(qubits, inds):
115+
self.add_meas(qubit)
116+
if self.param("assign_error_flag", qubit):
117+
prob: float = self.param("assign_error_prob", qubit)
94118
circ.append(
95-
CircuitInstruction(SQ_MEASUREMENTS[name], inds, [prob])
119+
CircuitInstruction(SQ_MEASUREMENTS[name], [ind], [prob])
96120
)
97121
else:
98-
circ.append(CircuitInstruction(SQ_MEASUREMENTS[name], inds))
99-
else:
100-
for qubit, ind in zip(qubits, inds):
101-
prob: float = self.param(f"{name}_error_prob", qubit)
102-
circ.append(CircuitInstruction(noise_name, [ind], [prob]))
103-
for qubit, ind in zip(qubits, inds):
104-
self.add_meas(qubit)
105-
if self.param("assign_error_flag", qubit):
106-
prob: float = self.param("assign_error_prob", qubit)
107-
circ.append(
108-
CircuitInstruction(SQ_MEASUREMENTS[name], [ind], [prob])
109-
)
110-
else:
111-
circ.append(
112-
CircuitInstruction(SQ_MEASUREMENTS[name], [ind])
113-
)
114-
115-
return circ
122+
circ.append(CircuitInstruction(SQ_MEASUREMENTS[name], [ind]))
116123

117-
return sq_meas
118-
119-
elif name in SQ_RESETS:
124+
return circ
120125

121-
def sq_reset(qubits: Collection[str]) -> Circuit:
122-
inds = self.get_inds(qubits)
123-
noise_name = "X_ERROR" if "_x" not in name else "Z_ERROR"
124-
circ = Circuit()
126+
return sq_meas
125127

126-
circ.append(CircuitInstruction(SQ_RESETS[name], inds))
127-
if self.uniform:
128-
prob: float = self.param(f"{name}_error_prob")
129-
circ.append(CircuitInstruction(noise_name, inds, [prob]))
130-
else:
131-
for qubit, ind in zip(qubits, inds):
132-
prob: float = self.param(f"{name}_error_prob", qubit)
133-
circ.append(CircuitInstruction(noise_name, [ind], [prob]))
128+
def _sq_reset_generator(self, name: str):
129+
def sq_reset(qubits: Collection[str]) -> Circuit:
130+
inds = self.get_inds(qubits)
131+
noise_name = "X_ERROR" if "_x" not in name else "Z_ERROR"
132+
circ = Circuit()
134133

135-
return circ
134+
circ.append(CircuitInstruction(SQ_RESETS[name], inds))
135+
if self.uniform:
136+
prob: float = self.param(f"{name}_error_prob")
137+
circ.append(CircuitInstruction(noise_name, inds, [prob]))
138+
else:
139+
for qubit, ind in zip(qubits, inds):
140+
prob: float = self.param(f"{name}_error_prob", qubit)
141+
circ.append(CircuitInstruction(noise_name, [ind], [prob]))
136142

137-
return sq_reset
143+
return circ
138144

139-
return attr
145+
return sq_reset
140146

141147
@override
142148
def idle_noise(
@@ -166,29 +172,29 @@ def __getattribute__(self, name: str) -> object:
166172
attr = super().__getattribute__(name)
167173

168174
if name == "swap" and callable(attr):
175+
return self._swap_generator(name)
169176

170-
def swap(qubits: Collection[str]) -> Circuit:
171-
if len(qubits) % 2 != 0:
172-
raise ValueError("Expected and even number of qubits.")
177+
return attr
173178

174-
inds = self.get_inds(qubits)
175-
circ = Circuit()
179+
def _swap_generator(self, name: str):
180+
def swap(qubits: Collection[str]) -> Circuit:
181+
if len(qubits) % 2 != 0:
182+
raise ValueError("Expected and even number of qubits.")
176183

177-
circ.append(CircuitInstruction("SWAP", inds))
178-
if self.uniform:
179-
prob: float = self.param("swap_error_prob")
180-
circ.append(CircuitInstruction("DEPOLARIZE1", inds, [prob]))
181-
else:
182-
for qubit_pair, ind_pair in zip(
183-
grouper(qubits, 2), grouper(inds, 2)
184-
):
185-
prob: float = self.param("swap_error_prob", qubit_pair)
186-
circ.append(CircuitInstruction("DEPOLARIZE1", ind_pair, [prob]))
187-
return circ
184+
inds = self.get_inds(qubits)
185+
circ = Circuit()
188186

189-
return swap
187+
circ.append(CircuitInstruction("SWAP", inds))
188+
if self.uniform:
189+
prob: float = self.param("swap_error_prob")
190+
circ.append(CircuitInstruction("DEPOLARIZE1", inds, [prob]))
191+
else:
192+
for qubit_pair, ind_pair in zip(grouper(qubits, 2), grouper(inds, 2)):
193+
prob: float = self.param("swap_error_prob", qubit_pair)
194+
circ.append(CircuitInstruction("DEPOLARIZE1", ind_pair, [prob]))
195+
return circ
190196

191-
return attr
197+
return swap
192198

193199

194200
class SD6NoiseModel(CircuitNoiseModel):
@@ -268,10 +274,10 @@ class SI1000NoiseModel(CircuitNoiseModel):
268274

269275
DEFAULT_SETUP = SI1000()
270276

271-
def __init__(self, qubit_inds: dict[str, int], setup: Setup | None = None) -> None:
277+
def new_circuit(self) -> None:
272278
self._meas_or_reset_qubits: list[str] = []
273279
self._meas_reset_ops: list[str] = list(SQ_MEASUREMENTS) + list(SQ_RESETS)
274-
super().__init__(qubit_inds=qubit_inds, setup=setup)
280+
super().new_circuit()
275281
return
276282

277283
@override
@@ -281,7 +287,7 @@ def __getattribute__(self, name: str) -> object:
281287
if callable(attr) and (name in ALL_OPS):
282288
if name not in self._supported_operations:
283289
raise ValueError(
284-
f"Operation {name} is not available in the SI1000 noise model."
290+
f"Operation {name} is not available in the {self.__class__.__name__} noise model."
285291
)
286292

287293
if name in self._meas_reset_ops:
@@ -304,6 +310,58 @@ def flush_noise(self) -> Circuit:
304310
return circ
305311

306312

313+
class NLRNoiseModel(SI1000NoiseModel):
314+
"""
315+
The NLR noise model is defined in the following paper:
316+
317+
Beni, L. A., Higgott, O., & Shutty, N. (2025).
318+
Tesseract: A search-based decoder for quantum error correction.
319+
arXiv preprint arXiv:2503.10988.
320+
321+
which corresponds to a ``SI1000`` noise model but with higher noise
322+
strength for the two-qubit depolarizing channels after the long-range CZ gates.
323+
324+
See the documentation for the ``SI1000NoiseModel`` for more information.
325+
"""
326+
327+
DEFAULT_SETUP = NLR()
328+
329+
def __init__(
330+
self,
331+
qubit_inds: dict[str, int],
332+
qubit_coords: dict[str, Sequence[float | int]],
333+
setup: Setup | None = None,
334+
) -> None:
335+
self._qubit_coords: dict[str, Sequence[float | int]] = qubit_coords
336+
super().__init__(qubit_inds=qubit_inds, setup=setup)
337+
return
338+
339+
@override
340+
def __getattribute__(self, name: str) -> object:
341+
attr = super().__getattribute__(name)
342+
343+
if callable(attr) and (name in ("cz", "cphase")):
344+
return self._cphase_generator(name)
345+
346+
return attr
347+
348+
def _cphase_generator(self, name: str):
349+
def cphase(qubits: Collection[str]) -> Circuit:
350+
circuit = Circuit()
351+
for pair in grouper(qubits, 2):
352+
distance = math.dist(
353+
self._qubit_coords[pair[0]], self._qubit_coords[pair[1]]
354+
)
355+
_name = name
356+
if distance > self.setup.param("long_coupler_distance"):
357+
_name = f"long_range_{name}"
358+
attr = self._tq_gate_generator(_name)
359+
circuit += attr(pair)
360+
return circuit
361+
362+
return cphase
363+
364+
307365
class BiasedCircuitNoiseModel(Model):
308366
DEFAULT_SETUP = BiasedCircuitNoiseSetup()
309367

@@ -492,9 +550,9 @@ class T1T2NoiseModel(Model):
492550
``T1T2NoiseModel.tick``.
493551
"""
494552

495-
def __init__(self, qubit_inds: dict[str, int], setup: Setup | None = None) -> None:
496-
self._durations: dict[str, float] = {q: 0.0 for q in qubit_inds}
497-
super().__init__(setup=setup, qubit_inds=qubit_inds)
553+
def new_circuit(self) -> None:
554+
self._durations: dict[str, float] = {q: 0.0 for q in self._qubit_inds}
555+
super().new_circuit()
498556
return
499557

500558
def _generic_gate(self, name: str, qubits: Collection[str]) -> Circuit:

0 commit comments

Comments
 (0)