mps-xtrap 1.0.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.
- mps_xtrap/__init__.py +44 -0
- mps_xtrap/circuits/__init__.py +336 -0
- mps_xtrap/cli.py +265 -0
- mps_xtrap/core/__init__.py +1 -0
- mps_xtrap/core/mps.py +347 -0
- mps_xtrap/examples/__init__.py +0 -0
- mps_xtrap/examples/examples.py +174 -0
- mps_xtrap/extrapolation/__init__.py +462 -0
- mps_xtrap/gates/__init__.py +224 -0
- mps_xtrap/py.typed +0 -0
- mps_xtrap/tests/__init__.py +0 -0
- mps_xtrap/tests/test_all.py +404 -0
- mps_xtrap/tests/test_production.py +988 -0
- mps_xtrap-1.0.0.dist-info/LICENSE +21 -0
- mps_xtrap-1.0.0.dist-info/METADATA +197 -0
- mps_xtrap-1.0.0.dist-info/RECORD +19 -0
- mps_xtrap-1.0.0.dist-info/WHEEL +5 -0
- mps_xtrap-1.0.0.dist-info/entry_points.txt +2 -0
- mps_xtrap-1.0.0.dist-info/top_level.txt +1 -0
mps_xtrap/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mps_xtrap — MPS-based quantum circuit simulator with Richardson extrapolation.
|
|
3
|
+
|
|
4
|
+
Quick start
|
|
5
|
+
-----------
|
|
6
|
+
>>> from mps_xtrap import Circuit, MPSSimulator, MultiChiRunner, SweepConfig
|
|
7
|
+
>>>
|
|
8
|
+
>>> # Simple simulation at a fixed bond dimension
|
|
9
|
+
>>> c = Circuit(4)
|
|
10
|
+
>>> c.h(0).cx(0,1).cx(1,2).cx(2,3)
|
|
11
|
+
>>> sim = MPSSimulator(chi=64)
|
|
12
|
+
>>> state = sim.run(c)
|
|
13
|
+
>>> print(state.expectation_pauli_z(0))
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Extrapolated simulation — just set base_chi, scaling_factor, n_levels
|
|
16
|
+
>>> # Bond dimensions are auto-generated: [16, 32, 64]
|
|
17
|
+
>>> runner = MultiChiRunner(SweepConfig(base_chi=16, scaling_factor=2, n_levels=3))
|
|
18
|
+
>>> result = runner.run(c, {'Z0': ('Z', 0), 'Z1': ('Z', 1)})
|
|
19
|
+
>>> print(result.summary())
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .core.mps import MPS
|
|
23
|
+
from .circuits import Circuit, MPSSimulator
|
|
24
|
+
from .extrapolation import (
|
|
25
|
+
MultiChiRunner,
|
|
26
|
+
SweepConfig,
|
|
27
|
+
ExtrapolationResult,
|
|
28
|
+
MultiObservableResult,
|
|
29
|
+
)
|
|
30
|
+
from .gates import get_gate
|
|
31
|
+
|
|
32
|
+
__version__ = "1.0.0"
|
|
33
|
+
__author__ = "MPS Sim"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"MPS",
|
|
37
|
+
"Circuit",
|
|
38
|
+
"MPSSimulator",
|
|
39
|
+
"MultiChiRunner",
|
|
40
|
+
"SweepConfig",
|
|
41
|
+
"ExtrapolationResult",
|
|
42
|
+
"MultiObservableResult",
|
|
43
|
+
"get_gate",
|
|
44
|
+
]
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum circuit representation and gate application on MPS.
|
|
3
|
+
|
|
4
|
+
A Circuit is an ordered list of gate instructions. The Simulator
|
|
5
|
+
applies these instructions to an MPS, performing SVD truncations
|
|
6
|
+
at two-qubit gates.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import List, Tuple, Optional, Union, Dict, Any
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
from ..core.mps import MPS
|
|
17
|
+
from ..gates import get_gate, SINGLE_QUBIT_GATES, TWO_QUBIT_GATES
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Gate instruction
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class GateInstruction:
|
|
28
|
+
"""
|
|
29
|
+
A single gate application.
|
|
30
|
+
|
|
31
|
+
Attributes
|
|
32
|
+
----------
|
|
33
|
+
name : str
|
|
34
|
+
Gate name (e.g., 'H', 'CNOT', 'Rx').
|
|
35
|
+
qubits : list of int
|
|
36
|
+
Target qubits. Length 1 for single-qubit gates, 2 for two-qubit.
|
|
37
|
+
params : list of float
|
|
38
|
+
Gate parameters (e.g., rotation angles).
|
|
39
|
+
matrix : ndarray or None
|
|
40
|
+
Explicit gate matrix (overrides name lookup if provided).
|
|
41
|
+
label : str
|
|
42
|
+
Human-readable label for circuit diagrams.
|
|
43
|
+
"""
|
|
44
|
+
name: str
|
|
45
|
+
qubits: List[int]
|
|
46
|
+
params: List[float] = field(default_factory=list)
|
|
47
|
+
matrix: Optional[NDArray] = None
|
|
48
|
+
label: str = ""
|
|
49
|
+
|
|
50
|
+
def __post_init__(self):
|
|
51
|
+
if not self.label:
|
|
52
|
+
self.label = self.name
|
|
53
|
+
if not isinstance(self.qubits, list):
|
|
54
|
+
self.qubits = list(self.qubits)
|
|
55
|
+
|
|
56
|
+
def get_matrix(self) -> NDArray:
|
|
57
|
+
if self.matrix is not None:
|
|
58
|
+
return self.matrix
|
|
59
|
+
return get_gate(self.name, *self.params)
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
q = ",".join(map(str, self.qubits))
|
|
63
|
+
p = f"({','.join(f'{x:.4f}' for x in self.params)})" if self.params else ""
|
|
64
|
+
return f"{self.name}{p}[{q}]"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Circuit
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
class Circuit:
|
|
72
|
+
"""
|
|
73
|
+
A quantum circuit as an ordered list of gate instructions.
|
|
74
|
+
|
|
75
|
+
Supports a fluent builder API:
|
|
76
|
+
c = Circuit(4)
|
|
77
|
+
c.h(0).cx(0,1).rz(np.pi/4, 2).measure_all()
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, n_qubits: int):
|
|
81
|
+
self.n = n_qubits
|
|
82
|
+
self.instructions: List[GateInstruction] = []
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# Builder API
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def _add(self, name: str, qubits, params=None, matrix=None) -> 'Circuit':
|
|
89
|
+
if isinstance(qubits, int):
|
|
90
|
+
qubits = [qubits]
|
|
91
|
+
inst = GateInstruction(
|
|
92
|
+
name=name,
|
|
93
|
+
qubits=list(qubits),
|
|
94
|
+
params=list(params) if params else [],
|
|
95
|
+
matrix=matrix,
|
|
96
|
+
)
|
|
97
|
+
self.instructions.append(inst)
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
# Single-qubit gates
|
|
101
|
+
def i(self, q): return self._add('I', [q])
|
|
102
|
+
def x(self, q): return self._add('X', [q])
|
|
103
|
+
def y(self, q): return self._add('Y', [q])
|
|
104
|
+
def z(self, q): return self._add('Z', [q])
|
|
105
|
+
def h(self, q): return self._add('H', [q])
|
|
106
|
+
def s(self, q): return self._add('S', [q])
|
|
107
|
+
def t(self, q): return self._add('T', [q])
|
|
108
|
+
def sdg(self, q): return self._add('Sdg', [q])
|
|
109
|
+
def tdg(self, q): return self._add('Tdg', [q])
|
|
110
|
+
|
|
111
|
+
def rx(self, theta: float, q: int): return self._add('Rx', [q], [theta])
|
|
112
|
+
def ry(self, theta: float, q: int): return self._add('Ry', [q], [theta])
|
|
113
|
+
def rz(self, theta: float, q: int): return self._add('Rz', [q], [theta])
|
|
114
|
+
def p(self, phi: float, q: int): return self._add('P', [q], [phi])
|
|
115
|
+
def u(self, theta, phi, lam, q): return self._add('U', [q], [theta, phi, lam])
|
|
116
|
+
|
|
117
|
+
# Two-qubit gates
|
|
118
|
+
def cx(self, ctrl, tgt): return self._add('CNOT', [ctrl, tgt])
|
|
119
|
+
def cnot(self, ctrl, tgt): return self._add('CNOT', [ctrl, tgt])
|
|
120
|
+
def cz(self, q1, q2): return self._add('CZ', [q1, q2])
|
|
121
|
+
def swap(self, q1, q2): return self._add('SWAP', [q1, q2])
|
|
122
|
+
def iswap(self, q1, q2): return self._add('iSWAP', [q1, q2])
|
|
123
|
+
def xx(self, theta, q1, q2): return self._add('XX', [q1, q2], [theta])
|
|
124
|
+
def yy(self, theta, q1, q2): return self._add('YY', [q1, q2], [theta])
|
|
125
|
+
def zz(self, theta, q1, q2): return self._add('ZZ', [q1, q2], [theta])
|
|
126
|
+
def crz(self, theta, ctrl, tgt): return self._add('CRz', [ctrl, tgt], [theta])
|
|
127
|
+
def cp(self, phi, ctrl, tgt): return self._add('CP', [ctrl, tgt], [phi])
|
|
128
|
+
|
|
129
|
+
# Custom matrix
|
|
130
|
+
def unitary(self, matrix: NDArray, qubits): return self._add('U_custom', qubits, matrix=matrix)
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
# Helpers for common circuit patterns
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def barrier(self, *qubits):
|
|
137
|
+
"""Barrier is a no-op for simulation but marks a layer boundary."""
|
|
138
|
+
return self # no-op
|
|
139
|
+
|
|
140
|
+
def __len__(self):
|
|
141
|
+
return len(self.instructions)
|
|
142
|
+
|
|
143
|
+
def depth(self) -> int:
|
|
144
|
+
"""Estimate circuit depth (number of non-overlapping gate layers)."""
|
|
145
|
+
last_use = [-1] * self.n
|
|
146
|
+
max_layer = -1
|
|
147
|
+
for inst in self.instructions:
|
|
148
|
+
d = max(last_use[q] for q in inst.qubits) + 1
|
|
149
|
+
max_layer = max(max_layer, d)
|
|
150
|
+
for q in inst.qubits:
|
|
151
|
+
last_use[q] = d
|
|
152
|
+
return max_layer + 1 # convert 0-indexed layer to count
|
|
153
|
+
|
|
154
|
+
def two_qubit_count(self) -> int:
|
|
155
|
+
return sum(1 for inst in self.instructions if len(inst.qubits) == 2)
|
|
156
|
+
|
|
157
|
+
def __repr__(self) -> str:
|
|
158
|
+
return (f"Circuit(n={self.n}, gates={len(self.instructions)}, "
|
|
159
|
+
f"depth≈{self.depth()}, 2q={self.two_qubit_count()})")
|
|
160
|
+
|
|
161
|
+
def draw(self) -> str:
|
|
162
|
+
"""Simple text-based circuit diagram."""
|
|
163
|
+
lines = [f"q{i}: " for i in range(self.n)]
|
|
164
|
+
for inst in self.instructions:
|
|
165
|
+
label = str(inst)
|
|
166
|
+
for i, q in enumerate(inst.qubits):
|
|
167
|
+
lines[q] += f"──{label}──"
|
|
168
|
+
# Pad other qubits
|
|
169
|
+
max_len = max(len(l) for l in lines)
|
|
170
|
+
lines = [l + '─' * (max_len - len(l)) for l in lines]
|
|
171
|
+
return '\n'.join(lines)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# MPS Simulator
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
class MPSSimulator:
|
|
179
|
+
"""
|
|
180
|
+
Applies a Circuit to an MPS state.
|
|
181
|
+
|
|
182
|
+
Two-qubit gates on non-adjacent qubits are handled by first
|
|
183
|
+
swapping qubits into adjacent positions, applying the gate,
|
|
184
|
+
then swapping back.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(self, chi: int, svd_threshold: float = 1e-14):
|
|
188
|
+
"""
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
chi : int
|
|
192
|
+
Maximum bond dimension for SVD truncation.
|
|
193
|
+
svd_threshold : float
|
|
194
|
+
Singular values below this are discarded before bond-dim cap.
|
|
195
|
+
"""
|
|
196
|
+
self.chi = chi
|
|
197
|
+
self.svd_threshold = svd_threshold
|
|
198
|
+
|
|
199
|
+
def run(self, circuit: Circuit) -> MPS:
|
|
200
|
+
"""
|
|
201
|
+
Simulate circuit from |0...0> and return final MPS state.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
MPS
|
|
206
|
+
Final state after all gates applied.
|
|
207
|
+
"""
|
|
208
|
+
state = MPS(circuit.n, self.chi)
|
|
209
|
+
self._apply_circuit(circuit, state)
|
|
210
|
+
return state
|
|
211
|
+
|
|
212
|
+
def run_from(self, circuit: Circuit, state: MPS) -> MPS:
|
|
213
|
+
"""Apply circuit to an existing MPS state (modifies a copy)."""
|
|
214
|
+
new_state = state.copy()
|
|
215
|
+
new_state.chi = self.chi
|
|
216
|
+
self._apply_circuit(circuit, new_state)
|
|
217
|
+
return new_state
|
|
218
|
+
|
|
219
|
+
def _apply_circuit(self, circuit: Circuit, state: MPS):
|
|
220
|
+
for inst in circuit.instructions:
|
|
221
|
+
if len(inst.qubits) == 1:
|
|
222
|
+
self._apply_single(inst, state)
|
|
223
|
+
elif len(inst.qubits) == 2:
|
|
224
|
+
self._apply_two(inst, state)
|
|
225
|
+
else:
|
|
226
|
+
raise ValueError(f"Gates on >2 qubits not yet supported: {inst}")
|
|
227
|
+
|
|
228
|
+
def _apply_single(self, inst: GateInstruction, state: MPS):
|
|
229
|
+
"""Apply single-qubit gate via matrix multiplication on physical index."""
|
|
230
|
+
q = inst.qubits[0]
|
|
231
|
+
gate = inst.get_matrix() # (2,2)
|
|
232
|
+
t = state.get_tensor(q) # (chi_l, 2, chi_r)
|
|
233
|
+
# Contract: new[l,s',r] = sum_s gate[s',s] * t[l,s,r]
|
|
234
|
+
new_t = np.einsum('sp,lpr->lsr', gate, t)
|
|
235
|
+
state.set_tensor(q, new_t)
|
|
236
|
+
state.center = None # Canonicality violated
|
|
237
|
+
|
|
238
|
+
def _apply_two(self, inst: GateInstruction, state: MPS):
|
|
239
|
+
"""Apply two-qubit gate with SVD truncation."""
|
|
240
|
+
q1, q2 = inst.qubits
|
|
241
|
+
|
|
242
|
+
# Handle non-adjacent qubits via SWAP chain
|
|
243
|
+
if abs(q1 - q2) != 1:
|
|
244
|
+
self._apply_two_nonlocal(inst, state)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Ensure q1 < q2
|
|
248
|
+
if q1 > q2:
|
|
249
|
+
q1, q2 = q2, q1
|
|
250
|
+
gate = inst.get_matrix() # (2,2,2,2): op[s1',s2',s1,s2]
|
|
251
|
+
# Swap qubit ordering
|
|
252
|
+
gate = gate.transpose(1, 0, 3, 2)
|
|
253
|
+
else:
|
|
254
|
+
gate = inst.get_matrix()
|
|
255
|
+
|
|
256
|
+
# Build theta: two-site tensor
|
|
257
|
+
A = state.get_tensor(q1) # (chi_l, 2, chi_m)
|
|
258
|
+
B = state.get_tensor(q2) # (chi_m, 2, chi_r)
|
|
259
|
+
theta = np.einsum('lir,rjs->lijs', A, B) # (chi_l, 2, 2, chi_r)
|
|
260
|
+
|
|
261
|
+
# Apply gate: theta'[l,s1',s2',r] = sum_{s1,s2} gate[s1',s2',s1,s2] * theta[l,s1,s2,r]
|
|
262
|
+
theta = np.einsum('abij,lijs->labs', gate, theta)
|
|
263
|
+
|
|
264
|
+
# SVD truncation
|
|
265
|
+
state.apply_svd_truncation(q1, theta, chi=self.chi,
|
|
266
|
+
svd_threshold=self.svd_threshold)
|
|
267
|
+
state.center = None
|
|
268
|
+
|
|
269
|
+
def _apply_two_nonlocal(self, inst: GateInstruction, state: MPS):
|
|
270
|
+
"""
|
|
271
|
+
Apply a gate on non-adjacent qubits via SWAP chain.
|
|
272
|
+
Brings qubits adjacent, applies gate, swaps back.
|
|
273
|
+
"""
|
|
274
|
+
from ..gates import SWAP as make_swap
|
|
275
|
+
q1, q2 = inst.qubits
|
|
276
|
+
if q1 > q2:
|
|
277
|
+
q1, q2 = q2, q1
|
|
278
|
+
|
|
279
|
+
# Bubble q2 adjacent to q1
|
|
280
|
+
swap_gate = make_swap()
|
|
281
|
+
swap_inst = GateInstruction('SWAP', [0, 1], matrix=swap_gate)
|
|
282
|
+
|
|
283
|
+
for pos in range(q2, q1 + 1, -1):
|
|
284
|
+
swap_inst.qubits = [pos - 1, pos]
|
|
285
|
+
swap_inst.matrix = swap_gate
|
|
286
|
+
self._apply_two(swap_inst, state)
|
|
287
|
+
|
|
288
|
+
# Now apply actual gate at (q1, q1+1)
|
|
289
|
+
modified_inst = GateInstruction(
|
|
290
|
+
inst.name, [q1, q1 + 1],
|
|
291
|
+
inst.params, inst.matrix, inst.label
|
|
292
|
+
)
|
|
293
|
+
self._apply_two(modified_inst, state)
|
|
294
|
+
|
|
295
|
+
# Swap back
|
|
296
|
+
for pos in range(q1 + 1, q2):
|
|
297
|
+
swap_inst.qubits = [pos, pos + 1]
|
|
298
|
+
self._apply_two(swap_inst, state)
|
|
299
|
+
|
|
300
|
+
def expectation_value(
|
|
301
|
+
self,
|
|
302
|
+
state: MPS,
|
|
303
|
+
observable: str,
|
|
304
|
+
site: int,
|
|
305
|
+
site2: Optional[int] = None,
|
|
306
|
+
) -> float:
|
|
307
|
+
"""
|
|
308
|
+
Convenience wrapper to compute expectation values.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
state : MPS
|
|
313
|
+
observable : str
|
|
314
|
+
'X', 'Y', 'Z', 'ZZ', 'XX', etc.
|
|
315
|
+
site : int
|
|
316
|
+
Primary site.
|
|
317
|
+
site2 : int, optional
|
|
318
|
+
Second site for two-qubit observables.
|
|
319
|
+
"""
|
|
320
|
+
if observable == 'Z':
|
|
321
|
+
return state.expectation_pauli_z(site)
|
|
322
|
+
elif observable == 'X':
|
|
323
|
+
return state.expectation_pauli_x(site)
|
|
324
|
+
elif observable == 'Y':
|
|
325
|
+
return state.expectation_pauli_y(site)
|
|
326
|
+
elif observable in ('ZZ', 'XX', 'YY') and site2 is not None:
|
|
327
|
+
from ..gates import Z, X, Y
|
|
328
|
+
ops = {'ZZ': Z, 'XX': X, 'YY': Y}
|
|
329
|
+
op1 = ops[observable]()
|
|
330
|
+
op = np.einsum('ij,kl->ikjl', op1, op1)
|
|
331
|
+
if abs(site - site2) == 1:
|
|
332
|
+
return float(np.real(state.expectation_two(op, min(site, site2))))
|
|
333
|
+
else:
|
|
334
|
+
raise NotImplementedError("Non-adjacent two-site observables not implemented")
|
|
335
|
+
else:
|
|
336
|
+
raise ValueError(f"Unknown observable: {observable}")
|
mps_xtrap/cli.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
mps_xtrap CLI — MPS quantum circuit simulator with Richardson extrapolation.
|
|
4
|
+
|
|
5
|
+
Usage
|
|
6
|
+
-----
|
|
7
|
+
python cli.py simulate --circuit bell --chi 64
|
|
8
|
+
python cli.py extrapolate --circuit ising --chi-base 16 --levels 3
|
|
9
|
+
python cli.py benchmark --circuit ghz --n 10
|
|
10
|
+
python cli.py info
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import os
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import numpy as np
|
|
19
|
+
import time
|
|
20
|
+
from mps_xtrap import Circuit, MPSSimulator, MultiChiRunner, SweepConfig
|
|
21
|
+
from mps_xtrap import RichardsonExtrapolator
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Built-in circuit library
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def make_bell(n=2) -> Circuit:
|
|
29
|
+
c = Circuit(max(2, n))
|
|
30
|
+
c.h(0).cx(0, 1)
|
|
31
|
+
return c
|
|
32
|
+
|
|
33
|
+
def make_ghz(n=6) -> Circuit:
|
|
34
|
+
c = Circuit(n)
|
|
35
|
+
c.h(0)
|
|
36
|
+
for i in range(n - 1):
|
|
37
|
+
c.cx(i, i + 1)
|
|
38
|
+
return c
|
|
39
|
+
|
|
40
|
+
def make_ising(n=8, steps=5, J=1.0, h=0.5, dt=0.05) -> Circuit:
|
|
41
|
+
c = Circuit(n)
|
|
42
|
+
for _ in range(steps):
|
|
43
|
+
for q in range(n):
|
|
44
|
+
c.rx(2 * h * dt, q)
|
|
45
|
+
for q in range(n - 1):
|
|
46
|
+
c.zz(2 * J * dt, q, q + 1)
|
|
47
|
+
return c
|
|
48
|
+
|
|
49
|
+
def make_qft(n=6) -> Circuit:
|
|
50
|
+
"""Quantum Fourier Transform."""
|
|
51
|
+
c = Circuit(n)
|
|
52
|
+
for i in range(n):
|
|
53
|
+
c.h(i)
|
|
54
|
+
for j in range(i + 1, n):
|
|
55
|
+
c.cp(np.pi / 2 ** (j - i), i, j)
|
|
56
|
+
# Bit reversal swaps
|
|
57
|
+
for i in range(n // 2):
|
|
58
|
+
c.swap(i, n - 1 - i)
|
|
59
|
+
return c
|
|
60
|
+
|
|
61
|
+
def make_random(n=8, depth=10, seed=42) -> Circuit:
|
|
62
|
+
np.random.seed(seed)
|
|
63
|
+
c = Circuit(n)
|
|
64
|
+
for _ in range(depth):
|
|
65
|
+
for q in range(n):
|
|
66
|
+
c.ry(np.random.uniform(0, np.pi), q)
|
|
67
|
+
for q in range(n - 1):
|
|
68
|
+
if np.random.random() < 0.5:
|
|
69
|
+
c.cx(q, q + 1)
|
|
70
|
+
return c
|
|
71
|
+
|
|
72
|
+
CIRCUITS = {
|
|
73
|
+
'bell': make_bell,
|
|
74
|
+
'ghz': make_ghz,
|
|
75
|
+
'ising': make_ising,
|
|
76
|
+
'qft': make_qft,
|
|
77
|
+
'random': make_random,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# CLI commands
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def cmd_simulate(args):
|
|
86
|
+
print(f"\n── Simulation ──────────────────────────────────────────")
|
|
87
|
+
circuit_fn = CIRCUITS.get(args.circuit)
|
|
88
|
+
if circuit_fn is None:
|
|
89
|
+
print(f"Unknown circuit '{args.circuit}'. Available: {list(CIRCUITS)}")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
c = circuit_fn(n=args.n)
|
|
93
|
+
print(f"Circuit: {c}")
|
|
94
|
+
print(f"Bond dim: χ={args.chi}")
|
|
95
|
+
|
|
96
|
+
t0 = time.time()
|
|
97
|
+
sim = MPSSimulator(chi=args.chi)
|
|
98
|
+
state = sim.run(c)
|
|
99
|
+
elapsed = time.time() - t0
|
|
100
|
+
|
|
101
|
+
print(f"\nResults (time: {elapsed:.2f}s):")
|
|
102
|
+
for q in range(min(c.n, 10)):
|
|
103
|
+
z = state.expectation_pauli_z(q)
|
|
104
|
+
x = state.expectation_pauli_x(q)
|
|
105
|
+
print(f" q{q}: <Z>={z:+.6f} <X>={x:+.6f}")
|
|
106
|
+
|
|
107
|
+
print(f"\nBond dims: {state.bond_dimensions()}")
|
|
108
|
+
print(f"Max bond dim reached: {state.max_bond_dim()} / {args.chi}")
|
|
109
|
+
print(f"Total truncation error: {state.total_truncation_error():.2e}")
|
|
110
|
+
print(f"State: {state}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cmd_extrapolate(args):
|
|
114
|
+
print(f"\n── Richardson Extrapolation ────────────────────────────")
|
|
115
|
+
circuit_fn = CIRCUITS.get(args.circuit)
|
|
116
|
+
if circuit_fn is None:
|
|
117
|
+
print(f"Unknown circuit '{args.circuit}'. Available: {list(CIRCUITS)}")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
c = circuit_fn(n=args.n)
|
|
121
|
+
print(f"Circuit: {c}")
|
|
122
|
+
|
|
123
|
+
cfg = SweepConfig(
|
|
124
|
+
base_chi=args.chi_base,
|
|
125
|
+
ratio=args.ratio,
|
|
126
|
+
n_levels=args.levels,
|
|
127
|
+
)
|
|
128
|
+
print(f"Bond dims: {cfg.bond_dims}")
|
|
129
|
+
print(f"Effective equivalent χ: {cfg.effective_chi()}")
|
|
130
|
+
print()
|
|
131
|
+
|
|
132
|
+
observables = {f'Z{q}': ('Z', q) for q in range(min(c.n, args.obs_qubits))}
|
|
133
|
+
runner = MultiChiRunner(cfg, verbose=True)
|
|
134
|
+
|
|
135
|
+
t0 = time.time()
|
|
136
|
+
result = runner.run(c, observables)
|
|
137
|
+
elapsed = time.time() - t0
|
|
138
|
+
|
|
139
|
+
print(f"\n{result.summary()}")
|
|
140
|
+
print(f"Total runtime: {elapsed:.2f}s")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cmd_benchmark(args):
|
|
144
|
+
print(f"\n── Benchmark ───────────────────────────────────────────")
|
|
145
|
+
circuit_fn = CIRCUITS.get(args.circuit)
|
|
146
|
+
if circuit_fn is None:
|
|
147
|
+
print(f"Unknown circuit '{args.circuit}'. Available: {list(CIRCUITS)}")
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
c = circuit_fn(n=args.n)
|
|
151
|
+
print(f"Circuit: {c}")
|
|
152
|
+
print(f"{'χ':>6} {'Time(s)':>10} {'<Z0>':>12} {'TruncErr':>12} {'MaxBond':>8}")
|
|
153
|
+
print("-" * 60)
|
|
154
|
+
|
|
155
|
+
chi_list = [args.chi_start * (2 ** i) for i in range(args.chi_levels)]
|
|
156
|
+
results = []
|
|
157
|
+
|
|
158
|
+
for chi in chi_list:
|
|
159
|
+
t0 = time.time()
|
|
160
|
+
sim = MPSSimulator(chi=chi)
|
|
161
|
+
state = sim.run(c)
|
|
162
|
+
elapsed = time.time() - t0
|
|
163
|
+
z0 = state.expectation_pauli_z(0)
|
|
164
|
+
trunc = state.total_truncation_error()
|
|
165
|
+
maxb = state.max_bond_dim()
|
|
166
|
+
results.append((chi, elapsed, z0, trunc, maxb))
|
|
167
|
+
print(f"{chi:>6} {elapsed:>10.3f} {z0:>+12.8f} {trunc:>12.2e} {maxb:>8}")
|
|
168
|
+
|
|
169
|
+
if len(results) >= 2:
|
|
170
|
+
print("\nRichardson extrapolation from all points:")
|
|
171
|
+
chi_vals = [r[0] for r in results]
|
|
172
|
+
z0_vals = [r[2] for r in results]
|
|
173
|
+
ext = RichardsonExtrapolator()
|
|
174
|
+
res = ext.extrapolate(chi_vals, z0_vals)
|
|
175
|
+
print(f" Extrapolated <Z0> = {res.extrapolated:.8f} ± {res.uncertainty:.2e}")
|
|
176
|
+
print(f" Alpha (error exponent) = {res.alpha:.3f}")
|
|
177
|
+
print(f" Reliable: {res.is_reliable}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def cmd_info(args):
|
|
181
|
+
print("""
|
|
182
|
+
mps_xtrap — MPS Quantum Circuit Simulator with Richardson Extrapolation
|
|
183
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
184
|
+
|
|
185
|
+
ABOUT
|
|
186
|
+
Simulates quantum circuits using Matrix Product States (MPS).
|
|
187
|
+
Unique feature: multilevel Richardson extrapolation to estimate
|
|
188
|
+
expectation values at arbitrarily high bond dimensions from
|
|
189
|
+
simulations at a geometric sequence of modest bond dimensions.
|
|
190
|
+
|
|
191
|
+
GATES SUPPORTED
|
|
192
|
+
Single-qubit: I, X, Y, Z, H, S, T, Sdg, Tdg, Rx, Ry, Rz, P, U
|
|
193
|
+
Two-qubit: CNOT/CX, CZ, SWAP, iSWAP, XX, YY, ZZ, CRz, CP
|
|
194
|
+
|
|
195
|
+
BUILT-IN CIRCUITS
|
|
196
|
+
bell — 2-qubit Bell state
|
|
197
|
+
ghz — N-qubit GHZ state
|
|
198
|
+
ising — Transverse-field Ising Trotterized evolution
|
|
199
|
+
qft — Quantum Fourier Transform
|
|
200
|
+
random — Random brick-layer circuit
|
|
201
|
+
|
|
202
|
+
EXTRAPOLATION THEORY
|
|
203
|
+
Truncation error scales as: ε(χ) ≈ a₁/χ^α + a₂/χ^2α + ...
|
|
204
|
+
Richardson extrapolation cancels successive error orders using
|
|
205
|
+
simulations at χ, 2χ, 4χ, ..., analogous to Romberg integration.
|
|
206
|
+
Reliability diagnostics flag when the power-law assumption breaks down.
|
|
207
|
+
|
|
208
|
+
EXAMPLES
|
|
209
|
+
python cli.py simulate --circuit ghz --n 10 --chi 64
|
|
210
|
+
python cli.py extrapolate --circuit ising --n 8 --chi-base 16 --levels 3
|
|
211
|
+
python cli.py benchmark --circuit ising --n 8 --chi-start 8 --chi-levels 4
|
|
212
|
+
""")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# Main
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def main():
|
|
220
|
+
parser = argparse.ArgumentParser(
|
|
221
|
+
description='mps_xtrap — MPS Quantum Simulator with Richardson Extrapolation'
|
|
222
|
+
)
|
|
223
|
+
sub = parser.add_subparsers(dest='command')
|
|
224
|
+
|
|
225
|
+
# simulate
|
|
226
|
+
p_sim = sub.add_parser('simulate', help='Run a single simulation')
|
|
227
|
+
p_sim.add_argument('--circuit', default='ghz', choices=list(CIRCUITS))
|
|
228
|
+
p_sim.add_argument('--n', type=int, default=8, help='Number of qubits')
|
|
229
|
+
p_sim.add_argument('--chi', type=int, default=64, help='Bond dimension')
|
|
230
|
+
|
|
231
|
+
# extrapolate
|
|
232
|
+
p_ext = sub.add_parser('extrapolate', help='Run multi-chi sweep with extrapolation')
|
|
233
|
+
p_ext.add_argument('--circuit', default='ising', choices=list(CIRCUITS))
|
|
234
|
+
p_ext.add_argument('--n', type=int, default=8)
|
|
235
|
+
p_ext.add_argument('--chi-base', type=int, default=16)
|
|
236
|
+
p_ext.add_argument('--ratio', type=float, default=2.0)
|
|
237
|
+
p_ext.add_argument('--levels', type=int, default=3)
|
|
238
|
+
p_ext.add_argument('--obs-qubits', type=int, default=4, help='Number of qubits to measure')
|
|
239
|
+
|
|
240
|
+
# benchmark
|
|
241
|
+
p_bench = sub.add_parser('benchmark', help='Benchmark across chi values')
|
|
242
|
+
p_bench.add_argument('--circuit', default='ising', choices=list(CIRCUITS))
|
|
243
|
+
p_bench.add_argument('--n', type=int, default=8)
|
|
244
|
+
p_bench.add_argument('--chi-start', type=int, default=8)
|
|
245
|
+
p_bench.add_argument('--chi-levels', type=int, default=4)
|
|
246
|
+
|
|
247
|
+
# info
|
|
248
|
+
sub.add_parser('info', help='Show package info and supported gates')
|
|
249
|
+
|
|
250
|
+
args = parser.parse_args()
|
|
251
|
+
|
|
252
|
+
if args.command == 'simulate':
|
|
253
|
+
cmd_simulate(args)
|
|
254
|
+
elif args.command == 'extrapolate':
|
|
255
|
+
cmd_extrapolate(args)
|
|
256
|
+
elif args.command == 'benchmark':
|
|
257
|
+
cmd_benchmark(args)
|
|
258
|
+
elif args.command == 'info':
|
|
259
|
+
cmd_info(args)
|
|
260
|
+
else:
|
|
261
|
+
parser.print_help()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == '__main__':
|
|
265
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .mps import MPS
|