tensorcircuit-nightly 1.0.2.dev20250108__py3-none-any.whl → 1.4.0.dev20251103__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.
Potentially problematic release.
This version of tensorcircuit-nightly might be problematic. Click here for more details.
- tensorcircuit/__init__.py +18 -2
- tensorcircuit/about.py +46 -0
- tensorcircuit/abstractcircuit.py +4 -0
- tensorcircuit/analogcircuit.py +413 -0
- tensorcircuit/applications/layers.py +1 -1
- tensorcircuit/applications/van.py +1 -1
- tensorcircuit/backends/abstract_backend.py +320 -7
- tensorcircuit/backends/cupy_backend.py +3 -1
- tensorcircuit/backends/jax_backend.py +102 -4
- tensorcircuit/backends/jax_ops.py +110 -1
- tensorcircuit/backends/numpy_backend.py +49 -3
- tensorcircuit/backends/pytorch_backend.py +92 -3
- tensorcircuit/backends/tensorflow_backend.py +102 -3
- tensorcircuit/basecircuit.py +157 -98
- tensorcircuit/circuit.py +115 -57
- tensorcircuit/cloud/local.py +1 -1
- tensorcircuit/cloud/quafu_provider.py +1 -1
- tensorcircuit/cloud/tencent.py +1 -1
- tensorcircuit/compiler/simple_compiler.py +2 -2
- tensorcircuit/cons.py +142 -21
- tensorcircuit/densitymatrix.py +43 -14
- tensorcircuit/experimental.py +387 -129
- tensorcircuit/fgs.py +282 -81
- tensorcircuit/gates.py +66 -22
- tensorcircuit/interfaces/__init__.py +1 -3
- tensorcircuit/interfaces/jax.py +189 -0
- tensorcircuit/keras.py +3 -3
- tensorcircuit/mpscircuit.py +154 -65
- tensorcircuit/quantum.py +868 -152
- tensorcircuit/quditcircuit.py +733 -0
- tensorcircuit/quditgates.py +618 -0
- tensorcircuit/results/counts.py +147 -20
- tensorcircuit/results/readout_mitigation.py +4 -1
- tensorcircuit/shadows.py +1 -1
- tensorcircuit/simplify.py +3 -1
- tensorcircuit/stabilizercircuit.py +479 -0
- tensorcircuit/templates/__init__.py +2 -0
- tensorcircuit/templates/blocks.py +2 -2
- tensorcircuit/templates/hamiltonians.py +174 -0
- tensorcircuit/templates/lattice.py +1789 -0
- tensorcircuit/timeevol.py +896 -0
- tensorcircuit/translation.py +10 -3
- tensorcircuit/utils.py +7 -0
- {tensorcircuit_nightly-1.0.2.dev20250108.dist-info → tensorcircuit_nightly-1.4.0.dev20251103.dist-info}/METADATA +73 -23
- tensorcircuit_nightly-1.4.0.dev20251103.dist-info/RECORD +96 -0
- {tensorcircuit_nightly-1.0.2.dev20250108.dist-info → tensorcircuit_nightly-1.4.0.dev20251103.dist-info}/WHEEL +1 -1
- {tensorcircuit_nightly-1.0.2.dev20250108.dist-info → tensorcircuit_nightly-1.4.0.dev20251103.dist-info}/top_level.txt +0 -1
- tensorcircuit_nightly-1.0.2.dev20250108.dist-info/RECORD +0 -115
- tests/__init__.py +0 -0
- tests/conftest.py +0 -67
- tests/test_backends.py +0 -1031
- tests/test_calibrating.py +0 -149
- tests/test_channels.py +0 -365
- tests/test_circuit.py +0 -1699
- tests/test_cloud.py +0 -219
- tests/test_compiler.py +0 -147
- tests/test_dmcircuit.py +0 -555
- tests/test_ensemble.py +0 -72
- tests/test_fgs.py +0 -310
- tests/test_gates.py +0 -156
- tests/test_interfaces.py +0 -429
- tests/test_keras.py +0 -160
- tests/test_miscs.py +0 -277
- tests/test_mpscircuit.py +0 -341
- tests/test_noisemodel.py +0 -156
- tests/test_qaoa.py +0 -86
- tests/test_qem.py +0 -152
- tests/test_quantum.py +0 -526
- tests/test_quantum_attr.py +0 -42
- tests/test_results.py +0 -347
- tests/test_shadows.py +0 -160
- tests/test_simplify.py +0 -46
- tests/test_templates.py +0 -218
- tests/test_torchnn.py +0 -99
- tests/test_van.py +0 -102
- {tensorcircuit_nightly-1.0.2.dev20250108.dist-info → tensorcircuit_nightly-1.4.0.dev20251103.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stabilizer circuit simulator using Stim backend
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
6
|
+
import numpy as np
|
|
7
|
+
import stim
|
|
8
|
+
|
|
9
|
+
from .abstractcircuit import AbstractCircuit
|
|
10
|
+
|
|
11
|
+
Tensor = Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StabilizerCircuit(AbstractCircuit):
|
|
15
|
+
"""
|
|
16
|
+
Quantum circuit simulator for stabilizer circuits using Stim backend.
|
|
17
|
+
Supports Clifford operations and measurements.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Add gate sets as class attributes
|
|
21
|
+
clifford_gates = ["h", "x", "y", "z", "cnot", "cz", "swap", "s", "sd"]
|
|
22
|
+
gate_map = {
|
|
23
|
+
"h": "H",
|
|
24
|
+
"x": "X",
|
|
25
|
+
"y": "Y",
|
|
26
|
+
"z": "Z",
|
|
27
|
+
"cnot": "CNOT",
|
|
28
|
+
"cz": "CZ",
|
|
29
|
+
"swap": "SWAP",
|
|
30
|
+
"s": "S",
|
|
31
|
+
"sd": "S_DAG",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self, nqubits: int, inputs: Tensor = None, tableau_inputs: Tensor = None
|
|
36
|
+
) -> None:
|
|
37
|
+
"""
|
|
38
|
+
``StabilizerCircuit`` class based on stim package
|
|
39
|
+
|
|
40
|
+
:param nqubits: Number of qubits
|
|
41
|
+
:type nqubits: int
|
|
42
|
+
:param inputs: initial state by stabilizers, defaults to None
|
|
43
|
+
:type inputs: Tensor, optional
|
|
44
|
+
:param tableau_inputs: initial state by **inverse** tableau, defaults to None
|
|
45
|
+
:type tableau_inputs: Tensor, optional
|
|
46
|
+
"""
|
|
47
|
+
self._nqubits = nqubits
|
|
48
|
+
self._stim_circuit = stim.Circuit()
|
|
49
|
+
self._qir: List[Dict[str, Any]] = []
|
|
50
|
+
self.is_dm = False
|
|
51
|
+
self.inputs = None
|
|
52
|
+
self._extra_qir: List[Dict[str, Any]] = []
|
|
53
|
+
self.current_sim = stim.TableauSimulator()
|
|
54
|
+
if inputs:
|
|
55
|
+
self.current_sim.set_state_from_stabilizers(inputs)
|
|
56
|
+
if tableau_inputs:
|
|
57
|
+
self.current_sim.set_inverse_tableau(tableau_inputs)
|
|
58
|
+
|
|
59
|
+
def apply_general_gate(
|
|
60
|
+
self,
|
|
61
|
+
gate: Any,
|
|
62
|
+
*index: int,
|
|
63
|
+
name: Optional[str] = None,
|
|
64
|
+
**kws: Any,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Apply a Clifford gate to the circuit.
|
|
68
|
+
|
|
69
|
+
:param gate: Gate to apply (must be Clifford)
|
|
70
|
+
:type gate: Any
|
|
71
|
+
:param index: Qubit indices to apply the gate to
|
|
72
|
+
:type index: int
|
|
73
|
+
:param name: Name of the gate operation, defaults to None
|
|
74
|
+
:type name: Optional[str], optional
|
|
75
|
+
:raises ValueError: If non-Clifford gate is applied
|
|
76
|
+
"""
|
|
77
|
+
if name is None:
|
|
78
|
+
name = ""
|
|
79
|
+
|
|
80
|
+
# Record gate in QIR
|
|
81
|
+
gate_dict = {
|
|
82
|
+
"gate": gate,
|
|
83
|
+
"index": index,
|
|
84
|
+
"name": name,
|
|
85
|
+
"split": None,
|
|
86
|
+
"mpo": False,
|
|
87
|
+
}
|
|
88
|
+
ir_dict = kws["ir_dict"]
|
|
89
|
+
if ir_dict is not None:
|
|
90
|
+
ir_dict.update(gate_dict)
|
|
91
|
+
else:
|
|
92
|
+
ir_dict = gate_dict
|
|
93
|
+
self._qir.append(ir_dict)
|
|
94
|
+
|
|
95
|
+
# Map TensorCircuit gates to Stim gates
|
|
96
|
+
|
|
97
|
+
if name.lower() in self.gate_map:
|
|
98
|
+
# self._stim_circuit.append(gate_map[name.lower()], list(index))
|
|
99
|
+
gn = self.gate_map[name.lower()]
|
|
100
|
+
instruction = f"{gn} {' '.join(map(str, index))}"
|
|
101
|
+
self._stim_circuit.append_from_stim_program_text(instruction)
|
|
102
|
+
# append is much slower
|
|
103
|
+
# self.current_sim.do(stim.Circuit(instruction))
|
|
104
|
+
getattr(self.current_sim, gn.lower())(*index)
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError(f"Gate {name} is not supported in stabilizer simulation")
|
|
107
|
+
|
|
108
|
+
apply = apply_general_gate
|
|
109
|
+
|
|
110
|
+
def state(self) -> Tensor:
|
|
111
|
+
"""
|
|
112
|
+
Return the wavefunction of the circuit.
|
|
113
|
+
Note that the state can have smaller qubit count if no gate is applied on later qubits
|
|
114
|
+
"""
|
|
115
|
+
tab = self.current_tableau()
|
|
116
|
+
return tab.to_state_vector(endian="big")
|
|
117
|
+
|
|
118
|
+
def random_gate(self, *index: int, recorded: bool = False) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Apply a random Clifford gate to the circuit.
|
|
121
|
+
This operation will not record in qir
|
|
122
|
+
|
|
123
|
+
:param index: Qubit indices to apply the gate to
|
|
124
|
+
:type index: int
|
|
125
|
+
:param recorded: Whether the gate is recorded in ``stim.Circuit``, defaults to False
|
|
126
|
+
:type recorded: bool, optional
|
|
127
|
+
"""
|
|
128
|
+
m = len(index)
|
|
129
|
+
t = stim.Tableau.random(m)
|
|
130
|
+
self.current_sim.do_tableau(t, index)
|
|
131
|
+
if recorded:
|
|
132
|
+
self._stim_circuit += t.to_circuit()
|
|
133
|
+
|
|
134
|
+
def tableau_gate(self, *index: int, tableau: Any, recorded: bool = False) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Apply a gate indicated by tableau to the circuit.
|
|
137
|
+
This operation will not record in qir
|
|
138
|
+
|
|
139
|
+
:param index: Qubit indices to apply the gate to
|
|
140
|
+
:type index: int
|
|
141
|
+
:param tableau: stim.Tableau representation of the gate
|
|
142
|
+
:type tableau: Any
|
|
143
|
+
:param recorded: Whether the gate is recorded in ``stim.Circuit``, defaults to False
|
|
144
|
+
:type recorded: bool, optional
|
|
145
|
+
"""
|
|
146
|
+
self.current_sim.do_tableau(tableau, index)
|
|
147
|
+
if recorded:
|
|
148
|
+
self._stim_circuit += tableau.to_circuit()
|
|
149
|
+
|
|
150
|
+
def measure(self, *index: int, with_prob: bool = False) -> Tensor:
|
|
151
|
+
"""
|
|
152
|
+
Measure qubits in the Z basis.
|
|
153
|
+
|
|
154
|
+
:param index: Indices of the qubits to measure.
|
|
155
|
+
:type index: int
|
|
156
|
+
:param with_prob: If True, returns the theoretical probability of the measurement outcome.
|
|
157
|
+
defaults to False
|
|
158
|
+
:type with_prob: bool, optional
|
|
159
|
+
:return: A tensor containing the measurement results.
|
|
160
|
+
If `with_prob` is True, a tuple containing the results and the probability is returned.
|
|
161
|
+
:rtype: Tensor
|
|
162
|
+
"""
|
|
163
|
+
# Convert negative indices
|
|
164
|
+
|
|
165
|
+
index = tuple([i for i in index if i >= 0])
|
|
166
|
+
|
|
167
|
+
# Add measurement instructions
|
|
168
|
+
s1 = self.current_simulator().copy()
|
|
169
|
+
# Sample once from the circuit using sampler
|
|
170
|
+
|
|
171
|
+
if with_prob:
|
|
172
|
+
num_random_measurements = 0
|
|
173
|
+
for i in index:
|
|
174
|
+
if s1.peek_z(i) == 0:
|
|
175
|
+
num_random_measurements += 1
|
|
176
|
+
probability = (0.5) ** num_random_measurements
|
|
177
|
+
|
|
178
|
+
m = s1.measure_many(*index)
|
|
179
|
+
if with_prob:
|
|
180
|
+
return m, probability
|
|
181
|
+
return m
|
|
182
|
+
|
|
183
|
+
def cond_measurement(self, index: int) -> Tensor:
|
|
184
|
+
"""
|
|
185
|
+
Measure a single qubit in the Z basis and collapse the state.
|
|
186
|
+
|
|
187
|
+
:param index: The index of the qubit to measure.
|
|
188
|
+
:type index: int
|
|
189
|
+
:return: The measurement result (0 or 1).
|
|
190
|
+
:rtype: Tensor
|
|
191
|
+
"""
|
|
192
|
+
# Convert negative indices
|
|
193
|
+
|
|
194
|
+
# Add measurement instructions
|
|
195
|
+
self._stim_circuit.append_from_stim_program_text("M " + str(index))
|
|
196
|
+
# self.current_sim = None
|
|
197
|
+
m = self.current_simulator().measure(index)
|
|
198
|
+
# Sample once from the circuit using sampler
|
|
199
|
+
|
|
200
|
+
return m
|
|
201
|
+
|
|
202
|
+
cond_measure = cond_measurement
|
|
203
|
+
|
|
204
|
+
def cond_measure_many(self, *index: int) -> Tensor:
|
|
205
|
+
"""
|
|
206
|
+
Measure multiple qubits in the Z basis and collapse the state.
|
|
207
|
+
|
|
208
|
+
:param index: The indices of the qubits to measure.
|
|
209
|
+
:type index: int
|
|
210
|
+
:return: A tensor containing the measurement results.
|
|
211
|
+
:rtype: Tensor
|
|
212
|
+
"""
|
|
213
|
+
# Convert negative indices
|
|
214
|
+
|
|
215
|
+
# Add measurement instructions
|
|
216
|
+
self._stim_circuit.append_from_stim_program_text(
|
|
217
|
+
"M " + " ".join(map(str, index))
|
|
218
|
+
)
|
|
219
|
+
# self.current_sim = None
|
|
220
|
+
m = self.current_simulator().measure_many(*index)
|
|
221
|
+
# Sample once from the circuit using sampler
|
|
222
|
+
|
|
223
|
+
return m
|
|
224
|
+
|
|
225
|
+
def sample(
|
|
226
|
+
self,
|
|
227
|
+
batch: Optional[int] = None,
|
|
228
|
+
**kws: Any,
|
|
229
|
+
) -> Tensor:
|
|
230
|
+
"""
|
|
231
|
+
Sample measurements from the circuit.
|
|
232
|
+
|
|
233
|
+
:param batch: Number of samples to take, defaults to None (single sample)
|
|
234
|
+
:type batch: Optional[int], optional
|
|
235
|
+
:return: Measurement results
|
|
236
|
+
:rtype: Tensor
|
|
237
|
+
"""
|
|
238
|
+
if batch is None:
|
|
239
|
+
batch = 1
|
|
240
|
+
c = self.current_circuit().copy()
|
|
241
|
+
for i in range(self._nqubits):
|
|
242
|
+
c.append("M", [i])
|
|
243
|
+
sampler = c.compile_sampler()
|
|
244
|
+
samples = sampler.sample(batch)
|
|
245
|
+
return np.array(samples)
|
|
246
|
+
|
|
247
|
+
def expectation_ps( # type: ignore
|
|
248
|
+
self,
|
|
249
|
+
x: Optional[Sequence[int]] = None,
|
|
250
|
+
y: Optional[Sequence[int]] = None,
|
|
251
|
+
z: Optional[Sequence[int]] = None,
|
|
252
|
+
**kws: Any,
|
|
253
|
+
) -> Any:
|
|
254
|
+
"""
|
|
255
|
+
Compute exact expectation value of Pauli string using stim's direct calculation.
|
|
256
|
+
|
|
257
|
+
:param x: Indices for Pauli X measurements
|
|
258
|
+
:type x: Optional[Sequence[int]], optional
|
|
259
|
+
:param y: Indices for Pauli Y measurements
|
|
260
|
+
:type y: Optional[Sequence[int]], optional
|
|
261
|
+
:param z: Indices for Pauli Z measurements
|
|
262
|
+
:type z: Optional[Sequence[int]], optional
|
|
263
|
+
:return: Expectation value
|
|
264
|
+
:rtype: float
|
|
265
|
+
"""
|
|
266
|
+
# Build Pauli string representation
|
|
267
|
+
pauli_str = ["I"] * self._nqubits
|
|
268
|
+
|
|
269
|
+
if x:
|
|
270
|
+
for i in x:
|
|
271
|
+
pauli_str[i] = "X"
|
|
272
|
+
if y:
|
|
273
|
+
for i in y:
|
|
274
|
+
pauli_str[i] = "Y"
|
|
275
|
+
if z:
|
|
276
|
+
for i in z:
|
|
277
|
+
pauli_str[i] = "Z"
|
|
278
|
+
|
|
279
|
+
pauli_string = "".join(pauli_str)
|
|
280
|
+
# Calculate expectation using stim's direct method
|
|
281
|
+
expectation = self.current_simulator().peek_observable_expectation(
|
|
282
|
+
stim.PauliString(pauli_string)
|
|
283
|
+
)
|
|
284
|
+
return expectation
|
|
285
|
+
|
|
286
|
+
expps = expectation_ps
|
|
287
|
+
|
|
288
|
+
def sample_expectation_ps(
|
|
289
|
+
self,
|
|
290
|
+
x: Optional[Sequence[int]] = None,
|
|
291
|
+
y: Optional[Sequence[int]] = None,
|
|
292
|
+
z: Optional[Sequence[int]] = None,
|
|
293
|
+
shots: Optional[int] = None,
|
|
294
|
+
**kws: Any,
|
|
295
|
+
) -> float:
|
|
296
|
+
"""
|
|
297
|
+
Compute expectation value of Pauli string measurements.
|
|
298
|
+
|
|
299
|
+
:param x: Indices for Pauli X measurements, defaults to None
|
|
300
|
+
:type x: Optional[Sequence[int]], optional
|
|
301
|
+
:param y: Indices for Pauli Y measurements, defaults to None
|
|
302
|
+
:type y: Optional[Sequence[int]], optional
|
|
303
|
+
:param z: Indices for Pauli Z measurements, defaults to None
|
|
304
|
+
:type z: Optional[Sequence[int]], optional
|
|
305
|
+
:param shots: Number of measurement shots, defaults to None
|
|
306
|
+
:type shots: Optional[int], optional
|
|
307
|
+
:return: Expectation value
|
|
308
|
+
:rtype: float
|
|
309
|
+
"""
|
|
310
|
+
if shots is None:
|
|
311
|
+
shots = 1000 # Default number of shots
|
|
312
|
+
|
|
313
|
+
circuit = self._stim_circuit.copy()
|
|
314
|
+
|
|
315
|
+
# Add basis rotations for measurements
|
|
316
|
+
if x:
|
|
317
|
+
for i in x:
|
|
318
|
+
circuit.append("H", [i])
|
|
319
|
+
if y:
|
|
320
|
+
for i in y:
|
|
321
|
+
circuit.append("S_DAG", [i])
|
|
322
|
+
circuit.append("H", [i])
|
|
323
|
+
|
|
324
|
+
# Add measurements
|
|
325
|
+
measured_qubits: List[int] = []
|
|
326
|
+
if x:
|
|
327
|
+
measured_qubits.extend(x)
|
|
328
|
+
if y:
|
|
329
|
+
measured_qubits.extend(y)
|
|
330
|
+
if z:
|
|
331
|
+
measured_qubits.extend(z)
|
|
332
|
+
|
|
333
|
+
for i in measured_qubits:
|
|
334
|
+
circuit.append("M", [i])
|
|
335
|
+
|
|
336
|
+
# Sample and compute expectation using sampler
|
|
337
|
+
sampler = circuit.compile_sampler()
|
|
338
|
+
samples = sampler.sample(shots)
|
|
339
|
+
results = np.array(samples)
|
|
340
|
+
|
|
341
|
+
# Convert from {0,1} to {1,-1}
|
|
342
|
+
results = 1 - 2 * results
|
|
343
|
+
|
|
344
|
+
# Average over shots
|
|
345
|
+
expectation = np.mean(np.prod(results, axis=1))
|
|
346
|
+
|
|
347
|
+
return float(expectation)
|
|
348
|
+
|
|
349
|
+
sexpps = sample_expectation_ps
|
|
350
|
+
|
|
351
|
+
def mid_measurement(self, index: int, keep: int = 0) -> Tensor:
|
|
352
|
+
"""
|
|
353
|
+
Perform a mid-measurement operation on a qubit on z direction.
|
|
354
|
+
The post-selection cannot be recorded in ``stim.Circuit``
|
|
355
|
+
|
|
356
|
+
:param index: Index of the qubit to measure
|
|
357
|
+
:type index: int
|
|
358
|
+
:param keep: State of qubits to keep after measurement, defaults to 0 (up)
|
|
359
|
+
:type keep: int, optional
|
|
360
|
+
:return: Result of the mid-measurement operation
|
|
361
|
+
:rtype: Tensor
|
|
362
|
+
"""
|
|
363
|
+
if keep not in [0, 1]:
|
|
364
|
+
raise ValueError("keep must be 0 or 1")
|
|
365
|
+
|
|
366
|
+
self.current_sim.postselect_z(index, desired_value=keep)
|
|
367
|
+
|
|
368
|
+
mid_measure = mid_measurement
|
|
369
|
+
post_select = mid_measurement
|
|
370
|
+
post_selection = mid_measurement
|
|
371
|
+
|
|
372
|
+
def depolarizing(self, *index: int, p: float) -> None:
|
|
373
|
+
"""
|
|
374
|
+
Apply depolarizing noise to a qubit.
|
|
375
|
+
|
|
376
|
+
:param index: Index of the qubit to apply noise to
|
|
377
|
+
:type index: int
|
|
378
|
+
:param p: Noise parameter (probability of depolarizing)
|
|
379
|
+
:type p: float
|
|
380
|
+
"""
|
|
381
|
+
self._stim_circuit.append_from_stim_program_text(
|
|
382
|
+
f"DEPOLARIZE1({p}) {' '.join(map(str, index))}"
|
|
383
|
+
)
|
|
384
|
+
self.current_sim.depolarize1(*index, p=p)
|
|
385
|
+
|
|
386
|
+
def current_simulator(self) -> stim.TableauSimulator:
|
|
387
|
+
"""
|
|
388
|
+
Return the current simulator of the circuit.
|
|
389
|
+
"""
|
|
390
|
+
return self.current_sim
|
|
391
|
+
|
|
392
|
+
def current_circuit(self) -> stim.Circuit:
|
|
393
|
+
"""
|
|
394
|
+
Return the current stim circuit representation of the circuit.
|
|
395
|
+
"""
|
|
396
|
+
return self._stim_circuit
|
|
397
|
+
|
|
398
|
+
def current_tableau(self) -> stim.Tableau:
|
|
399
|
+
"""
|
|
400
|
+
Return the current tableau of the circuit.
|
|
401
|
+
"""
|
|
402
|
+
return self.current_simulator().current_inverse_tableau() ** -1
|
|
403
|
+
|
|
404
|
+
def current_inverse_tableau(self) -> stim.Tableau:
|
|
405
|
+
"""
|
|
406
|
+
Return the current inverse tableau of the circuit.
|
|
407
|
+
"""
|
|
408
|
+
return self.current_simulator().current_inverse_tableau()
|
|
409
|
+
|
|
410
|
+
def entanglement_entropy(self, cut: Sequence[int]) -> float:
|
|
411
|
+
"""
|
|
412
|
+
Calculate the entanglement entropy for a subset of qubits using stabilizer formalism.
|
|
413
|
+
|
|
414
|
+
:param cut: Indices of qubits to calculate entanglement entropy for
|
|
415
|
+
:type cut: Sequence[int]
|
|
416
|
+
:return: Entanglement entropy
|
|
417
|
+
:rtype: float
|
|
418
|
+
"""
|
|
419
|
+
# Get stabilizer tableau
|
|
420
|
+
tableau = self.current_tableau()
|
|
421
|
+
N = len(tableau)
|
|
422
|
+
|
|
423
|
+
# Pre-allocate binary matrix with proper dtype
|
|
424
|
+
# binary_matrix = np.zeros((N, 2 * N), dtype=np.int8)
|
|
425
|
+
|
|
426
|
+
# Vectorized conversion of stabilizers to binary matrix
|
|
427
|
+
# z_outputs = np.array([tableau.z_output(k) for k in range(N)])
|
|
428
|
+
# x_part = z_outputs == 1 # X
|
|
429
|
+
# z_part = z_outputs == 3 # Z
|
|
430
|
+
# y_part = z_outputs == 2 # Y
|
|
431
|
+
|
|
432
|
+
# binary_matrix[:, :N] = x_part | y_part
|
|
433
|
+
# binary_matrix[:, N:] = z_part | y_part
|
|
434
|
+
|
|
435
|
+
_, _, z2x, z2z, _, _ = tableau.to_numpy()
|
|
436
|
+
binary_matrix = np.concatenate([z2x, z2z], axis=1)
|
|
437
|
+
# Get reduced matrix for the cut using boolean indexing
|
|
438
|
+
cut_set = set(cut)
|
|
439
|
+
cut_indices = np.array(
|
|
440
|
+
[i for i in range(N) if i in cut_set]
|
|
441
|
+
+ [i + N for i in range(N) if i in cut_set]
|
|
442
|
+
)
|
|
443
|
+
reduced_matrix = binary_matrix[:, cut_indices]
|
|
444
|
+
|
|
445
|
+
# Efficient rank calculation using Gaussian elimination
|
|
446
|
+
matrix = reduced_matrix.copy()
|
|
447
|
+
n_rows, n_cols = matrix.shape
|
|
448
|
+
rank = 0
|
|
449
|
+
row = 0
|
|
450
|
+
|
|
451
|
+
for col in range(n_cols):
|
|
452
|
+
# Vectorized pivot finding
|
|
453
|
+
pivot_rows = np.nonzero(matrix[row:, col])[0]
|
|
454
|
+
if len(pivot_rows) > 0:
|
|
455
|
+
pivot_row = pivot_rows[0] + row
|
|
456
|
+
|
|
457
|
+
# Swap rows if necessary
|
|
458
|
+
if pivot_row != row:
|
|
459
|
+
matrix[row], matrix[pivot_row] = (
|
|
460
|
+
matrix[pivot_row].copy(),
|
|
461
|
+
matrix[row].copy(),
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Vectorized elimination
|
|
465
|
+
eliminate_mask = matrix[row + 1 :, col] == 1
|
|
466
|
+
matrix[row + 1 :][eliminate_mask] ^= matrix[row]
|
|
467
|
+
|
|
468
|
+
rank += 1
|
|
469
|
+
row += 1
|
|
470
|
+
|
|
471
|
+
if row == n_rows:
|
|
472
|
+
break
|
|
473
|
+
|
|
474
|
+
# Calculate entropy
|
|
475
|
+
return float((rank - len(cut)) * np.log(2))
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# Call _meta_apply at module level to register the gates
|
|
479
|
+
StabilizerCircuit._meta_apply()
|
|
@@ -91,7 +91,7 @@ def QAOA_block(
|
|
|
91
91
|
e2,
|
|
92
92
|
unitary=G._zz_matrix,
|
|
93
93
|
theta=paramzz * g[e1][e2].get("weight", 1.0),
|
|
94
|
-
**kws
|
|
94
|
+
**kws,
|
|
95
95
|
)
|
|
96
96
|
else:
|
|
97
97
|
i = 0
|
|
@@ -157,7 +157,7 @@ def qft(
|
|
|
157
157
|
*index: int,
|
|
158
158
|
do_swaps: bool = True,
|
|
159
159
|
inverse: bool = False,
|
|
160
|
-
insert_barriers: bool = False
|
|
160
|
+
insert_barriers: bool = False,
|
|
161
161
|
) -> Circuit:
|
|
162
162
|
"""
|
|
163
163
|
This function applies quantum fourier transformation (QFT) to the selected circuit lines
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from typing import Any, List, Tuple, Union
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ..cons import dtypestr, backend
|
|
4
|
+
from ..quantum import PauliStringSum2COO
|
|
5
|
+
from .lattice import AbstractLattice
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _create_empty_sparse_matrix(shape: Tuple[int, int]) -> Any:
|
|
9
|
+
"""
|
|
10
|
+
Helper function to create a backend-agnostic empty sparse matrix.
|
|
11
|
+
"""
|
|
12
|
+
indices = backend.convert_to_tensor(backend.zeros((0, 2), dtype="int32"))
|
|
13
|
+
values = backend.convert_to_tensor(backend.zeros((0,), dtype=dtypestr)) # type: ignore
|
|
14
|
+
return backend.coo_sparse_matrix(indices=indices, values=values, shape=shape) # type: ignore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def heisenberg_hamiltonian(
|
|
18
|
+
lattice: AbstractLattice,
|
|
19
|
+
j_coupling: Union[float, List[float], Tuple[float, ...]] = 1.0,
|
|
20
|
+
interaction_scope: str = "neighbors",
|
|
21
|
+
) -> Any:
|
|
22
|
+
r"""
|
|
23
|
+
Generates the sparse matrix of the Heisenberg Hamiltonian for a given lattice.
|
|
24
|
+
|
|
25
|
+
The Heisenberg Hamiltonian is defined as:
|
|
26
|
+
:math:`H = J\sum_{i,j} (X_i X_j + Y_i Y_j + Z_i Z_j)`
|
|
27
|
+
where the sum is over a specified set of interacting pairs {i,j}.
|
|
28
|
+
|
|
29
|
+
:param lattice: An instance of a class derived from AbstractLattice,
|
|
30
|
+
which provides the geometric information of the system.
|
|
31
|
+
:type lattice: AbstractLattice
|
|
32
|
+
:param j_coupling: The coupling constants. Can be a single float for an
|
|
33
|
+
isotropic model (Jx=Jy=Jz) or a list/tuple of 3 floats for an
|
|
34
|
+
anisotropic model (Jx, Jy, Jz). Defaults to 1.0.
|
|
35
|
+
:type j_coupling: Union[float, List[float], Tuple[float, ...]], optional
|
|
36
|
+
:param interaction_scope: Defines the range of interactions.
|
|
37
|
+
- "neighbors": Includes only nearest-neighbor pairs (default).
|
|
38
|
+
- "all": Includes all unique pairs of sites.
|
|
39
|
+
:type interaction_scope: str, optional
|
|
40
|
+
:return: The Hamiltonian as a backend-agnostic sparse matrix.
|
|
41
|
+
:rtype: Any
|
|
42
|
+
"""
|
|
43
|
+
num_sites = lattice.num_sites
|
|
44
|
+
if interaction_scope == "neighbors":
|
|
45
|
+
neighbor_pairs = lattice.get_neighbor_pairs(k=1, unique=True)
|
|
46
|
+
elif interaction_scope == "all":
|
|
47
|
+
neighbor_pairs = lattice.get_all_pairs()
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Invalid interaction_scope: '{interaction_scope}'. "
|
|
51
|
+
"Must be 'neighbors' or 'all'."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if isinstance(j_coupling, (float, int)):
|
|
55
|
+
js = [float(j_coupling)] * 3
|
|
56
|
+
else:
|
|
57
|
+
if len(j_coupling) != 3:
|
|
58
|
+
raise ValueError("j_coupling must be a float or a list/tuple of 3 floats.")
|
|
59
|
+
js = [float(j) for j in j_coupling]
|
|
60
|
+
|
|
61
|
+
if not neighbor_pairs:
|
|
62
|
+
return _create_empty_sparse_matrix(shape=(2**num_sites, 2**num_sites))
|
|
63
|
+
if num_sites == 0:
|
|
64
|
+
raise ValueError("Cannot generate a Hamiltonian for a lattice with zero sites.")
|
|
65
|
+
|
|
66
|
+
pauli_map = {"X": 1, "Y": 2, "Z": 3}
|
|
67
|
+
|
|
68
|
+
ls: List[List[int]] = []
|
|
69
|
+
weights: List[float] = []
|
|
70
|
+
|
|
71
|
+
pauli_terms = ["X", "Y", "Z"]
|
|
72
|
+
for i, j in neighbor_pairs:
|
|
73
|
+
for idx, pauli_char in enumerate(pauli_terms):
|
|
74
|
+
if abs(js[idx]) > 1e-9:
|
|
75
|
+
string = [0] * num_sites
|
|
76
|
+
string[i] = pauli_map[pauli_char]
|
|
77
|
+
string[j] = pauli_map[pauli_char]
|
|
78
|
+
ls.append(string)
|
|
79
|
+
weights.append(js[idx])
|
|
80
|
+
|
|
81
|
+
hamiltonian_matrix = PauliStringSum2COO(ls, weight=weights, numpy=False)
|
|
82
|
+
|
|
83
|
+
return hamiltonian_matrix
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def rydberg_hamiltonian(
|
|
87
|
+
lattice: AbstractLattice, omega: float, delta: float, c6: float
|
|
88
|
+
) -> Any:
|
|
89
|
+
r"""
|
|
90
|
+
Generates the sparse matrix of the Rydberg atom array Hamiltonian.
|
|
91
|
+
|
|
92
|
+
The Hamiltonian is defined as:
|
|
93
|
+
.. math::
|
|
94
|
+
|
|
95
|
+
H = \sum_i \frac{\Omega}{2} X_i
|
|
96
|
+
- \sum_i \frac{\delta}{2} \bigl(1 - Z_i \bigr)
|
|
97
|
+
+ \sum_{i<j} \frac{V_{ij}}{4} \bigl(1 - Z_i \bigr)\bigl(1 - Z_j \bigr)
|
|
98
|
+
|
|
99
|
+
= \sum_i \frac{\Omega}{2} X_i
|
|
100
|
+
+ \sum_i \frac{\delta}{2} Z_i
|
|
101
|
+
+ \sum_{i<j} \frac{V_{ij}}{4}\,\bigl(Z_i Z_j - Z_i - Z_j \bigr)
|
|
102
|
+
|
|
103
|
+
where :math:`V_{ij} = C6 / |r_i - r_j|^6`.
|
|
104
|
+
|
|
105
|
+
Note: Constant energy offset terms (proportional to the identity operator)
|
|
106
|
+
are ignored in this implementation.
|
|
107
|
+
|
|
108
|
+
:param lattice: An instance of a class derived from AbstractLattice,
|
|
109
|
+
which provides site coordinates and the distance matrix.
|
|
110
|
+
:type lattice: AbstractLattice
|
|
111
|
+
:param omega: The Rabi frequency (Ω) of the driving laser field.
|
|
112
|
+
:type omega: float
|
|
113
|
+
:param delta: The laser detuning (δ).
|
|
114
|
+
:type delta: float
|
|
115
|
+
:param c6: The Van der Waals interaction coefficient (C6).
|
|
116
|
+
:type c6: float
|
|
117
|
+
:return: The Hamiltonian as a backend-agnostic sparse matrix.
|
|
118
|
+
:rtype: Any
|
|
119
|
+
"""
|
|
120
|
+
num_sites = lattice.num_sites
|
|
121
|
+
if num_sites == 0:
|
|
122
|
+
raise ValueError("Cannot generate a Hamiltonian for a lattice with zero sites.")
|
|
123
|
+
|
|
124
|
+
pauli_map = {"X": 1, "Y": 2, "Z": 3}
|
|
125
|
+
ls: List[List[int]] = []
|
|
126
|
+
weights: List[float] = []
|
|
127
|
+
|
|
128
|
+
for i in range(num_sites):
|
|
129
|
+
x_string = [0] * num_sites
|
|
130
|
+
x_string[i] = pauli_map["X"]
|
|
131
|
+
ls.append(x_string)
|
|
132
|
+
weights.append(omega / 2.0)
|
|
133
|
+
|
|
134
|
+
z_coefficients = np.zeros(num_sites)
|
|
135
|
+
|
|
136
|
+
for i in range(num_sites):
|
|
137
|
+
z_coefficients[i] += delta / 2.0
|
|
138
|
+
|
|
139
|
+
dist_matrix = lattice.distance_matrix
|
|
140
|
+
|
|
141
|
+
for i in range(num_sites):
|
|
142
|
+
for j in range(i + 1, num_sites):
|
|
143
|
+
distance = dist_matrix[i, j]
|
|
144
|
+
|
|
145
|
+
if distance < 1e-9:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
interaction_strength = c6 / (distance**6)
|
|
149
|
+
coefficient = interaction_strength / 4.0
|
|
150
|
+
|
|
151
|
+
zz_string = [0] * num_sites
|
|
152
|
+
zz_string[i] = pauli_map["Z"]
|
|
153
|
+
zz_string[j] = pauli_map["Z"]
|
|
154
|
+
ls.append(zz_string)
|
|
155
|
+
weights.append(coefficient)
|
|
156
|
+
|
|
157
|
+
# The interaction term V_ij * n_i * n_j, when expanded using
|
|
158
|
+
# n_i = (1-Z_i)/2, becomes (V_ij/4)*(I - Z_i - Z_j + Z_i*Z_j).
|
|
159
|
+
# This contributes a positive term (+V_ij/4) to the ZZ interaction,
|
|
160
|
+
# but negative terms (-V_ij/4) to the single-site Z_i and Z_j operators.
|
|
161
|
+
|
|
162
|
+
z_coefficients[i] -= coefficient
|
|
163
|
+
z_coefficients[j] -= coefficient
|
|
164
|
+
|
|
165
|
+
for i in range(num_sites):
|
|
166
|
+
if abs(z_coefficients[i]) > 1e-9:
|
|
167
|
+
z_string = [0] * num_sites
|
|
168
|
+
z_string[i] = pauli_map["Z"]
|
|
169
|
+
ls.append(z_string)
|
|
170
|
+
weights.append(z_coefficients[i]) # type: ignore
|
|
171
|
+
|
|
172
|
+
hamiltonian_matrix = PauliStringSum2COO(ls, weight=weights, numpy=False)
|
|
173
|
+
|
|
174
|
+
return hamiltonian_matrix
|