Skip to content

Commit 36cabe4

Browse files
add symbol circuit class
1 parent 83b7ca7 commit 36cabe4

9 files changed

Lines changed: 2526 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212

1313
- Add detector support for `BaseCircuit`.
1414

15-
- Add `stim2tc` translation in translation.py
15+
- Add `stim2tc` translation in translation.py.
16+
17+
- Add `SymbolCircuit` class.
1618

1719
## v1.6.0
1820

docs/source/api/symbolcircuit.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
tensorcircuit.symbolcircuit
2+
================================================================================
3+
.. automodule:: tensorcircuit.symbolcircuit
4+
:members:
5+
:undoc-members:
6+
:show-inheritance:
7+
:inherited-members:

docs/source/api/symbolic_gates.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
tensorcircuit.symbolic_gates
2+
================================================================================
3+
.. automodule:: tensorcircuit.symbolic_gates
4+
:members:
5+
:undoc-members:
6+
:show-inheritance:
7+
:inherited-members:

docs/source/modules.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ tensorcircuit
2929
./api/shadows.rst
3030
./api/simplify.rst
3131
./api/stabilizercircuit.rst
32+
./api/symbolcircuit.rst
33+
./api/symbolic_gates.rst
3234
./api/templates.rst
3335
./api/timeevol.rst
3436
./api/torchnn.rst

examples/qaoa_symbolic.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
"""
2+
One-layer QAOA for a QUBO Hamiltonian — closed-form symbolic expectation.
3+
4+
This example uses ``tc.SymbolCircuit`` to derive the QAOA cost-function
5+
expectation
6+
7+
F(gamma, beta) = <psi(gamma, beta)| H_C |psi(gamma, beta)>
8+
9+
as a closed-form sympy expression in the two variational parameters gamma
10+
(phase separator) and beta (mixer), then cross-validates against a numeric
11+
tc.Circuit evaluation and plots the energy landscape.
12+
13+
Problem: Max-Cut on the 3-node path graph P3 (0 -- 1 -- 2)
14+
---------------------------------------------------------
15+
QUBO (minimisation form, variables x_i in {0, 1}):
16+
17+
min f(x) = Q_00 x_0 + Q_11 x_1 + Q_22 x_2
18+
+ Q_01 x_0 x_1 + Q_12 x_1 x_2
19+
20+
Q_diag = {0: -1, 1: -2, 2: -1}
21+
Q_off = {(0,1): +2, (1,2): +2}
22+
23+
(f(x) = -MaxCut(x), so minimising f maximises the cut.)
24+
25+
Ising mapping (x_i = (1 - Z_i) / 2, H_C = -f(x)):
26+
H_C = 1 - (Z_0 Z_1 + Z_1 Z_2) / 2 (to maximise)
27+
28+
QAOA ansatz (1 layer):
29+
|psi(gamma, beta)> = U_M(beta) U_C(gamma) |+>^3
30+
31+
U_C(gamma) = exp(-i gamma H_C)
32+
x RZZ(-gamma) on (0,1) and RZZ(-gamma) on (1,2)
33+
34+
U_M(beta) = prod_i RX(2 beta)
35+
36+
Run:
37+
conda run -n 2602 python examples/qaoa_symbolic.py
38+
"""
39+
40+
import sympy
41+
import numpy as np
42+
import matplotlib.pyplot as plt
43+
import tensorcircuit as tc
44+
45+
# ── QUBO → Ising helpers ──────────────────────────────────────────────────────
46+
47+
48+
def qubo_to_ising(Q_diag: dict, Q_off: dict):
49+
"""Convert QUBO coefficients to Ising (const, h, J) form.
50+
51+
QUBO: minimise sum_i Q_diag[i] x_i + sum_{i<j} Q_off[(i,j)] x_i x_j
52+
Ising: maximise const + sum_i h[i] Z_i + sum_{i<j} J[(i,j)] Z_i Z_j
53+
54+
The sign flip converts min→max.
55+
"""
56+
const = 0.0
57+
h: dict = {}
58+
J: dict = {}
59+
for i, q in Q_diag.items():
60+
# -q * x_i = -q * (1 - Z_i)/2 → const term -q/2, linear term +q/2 Z_i
61+
const -= q / 2
62+
h[i] = h.get(i, 0) + q / 2
63+
for (i, j), q in Q_off.items():
64+
# -q * x_i x_j = -q (1-Z_i)(1-Z_j)/4
65+
# constant: -q/4, linear: +q/4 Z_i + q/4 Z_j, quadratic: -q/4 Z_i Z_j
66+
const -= q / 4
67+
h[i] = h.get(i, 0) + q / 4
68+
h[j] = h.get(j, 0) + q / 4
69+
J[(i, j)] = J.get((i, j), 0) - q / 4
70+
return const, h, J
71+
72+
73+
# ── QAOA circuit builder ──────────────────────────────────────────────────────
74+
75+
76+
def build_qaoa(n, const, h, J, gamma, beta):
77+
"""Return a SymbolCircuit for 1-layer QAOA with symbolic (gamma, beta)."""
78+
tc.set_backend("numpy")
79+
sc = tc.SymbolCircuit(n)
80+
81+
# Initial state |+>^n = H^n |0>^n
82+
for i in range(n):
83+
sc.h(i)
84+
85+
# Cost unitary: exp(-i gamma H_C)
86+
# exp(-i gamma h_i Z_i) = RZ(2 gamma h_i)
87+
# exp(-i gamma J_ij Z_i Z_j) = RZZ(2 gamma J_ij)
88+
for i, hi in sorted(h.items()):
89+
if hi:
90+
sc.rz(i, theta=2 * hi * gamma)
91+
for (i, j), Jij in sorted(J.items()):
92+
if Jij:
93+
sc.rzz(i, j, theta=2 * Jij * gamma)
94+
95+
# Mixer unitary: exp(-i beta sum X_i) = prod RX(2 beta)
96+
for i in range(n):
97+
sc.rx(i, theta=2 * beta)
98+
99+
return sc
100+
101+
102+
# ── main ──────────────────────────────────────────────────────────────────────
103+
104+
105+
def main():
106+
# ── Problem ───────────────────────────────────────────────────────────────
107+
n = 3
108+
# MaxCut QUBO (minimisation): f(x) = -MaxCut(x), P3 path graph (0--1--2)
109+
# Q_diag[i] = -degree(i), Q_off[(i,j)] = +2 per edge
110+
Q_diag = {0: -1, 1: -2, 2: -1}
111+
Q_off = {(0, 1): 2, (1, 2): 2}
112+
113+
print("=" * 60)
114+
print("QUBO: minimise f(x) = x^T Q x (= -MaxCut(x))")
115+
print("Q_diag =", Q_diag)
116+
print("Q_off =", Q_off)
117+
print("(Minimising f = Maximising MaxCut; optimal cut = 2)")
118+
print()
119+
120+
# ── Ising Hamiltonian ─────────────────────────────────────────────────────
121+
const, h, J = qubo_to_ising(Q_diag, Q_off)
122+
print("Ising Hamiltonian H_C (to maximise):")
123+
terms = [f"{const:+.4f}"]
124+
for i, hi in sorted(h.items()):
125+
if hi:
126+
terms.append(f"{hi:+.4f} Z_{i}")
127+
for (i, j), Jij in sorted(J.items()):
128+
if Jij:
129+
terms.append(f"{Jij:+.4f} Z_{i}Z_{j}")
130+
print(" H_C =", " ".join(terms))
131+
print()
132+
133+
# ── Symbolic parameters ───────────────────────────────────────────────────
134+
gamma = sympy.Symbol("gamma", real=True)
135+
beta = sympy.Symbol("beta", real=True)
136+
137+
# ── Build circuit ─────────────────────────────────────────────────────────
138+
sc = build_qaoa(n, const, h, J, gamma, beta)
139+
print(
140+
f"QAOA circuit: {sc.gate_count()} gates, " f"free symbols: {sc.free_symbols()}"
141+
)
142+
print()
143+
144+
# ── Symbolic expectation F(gamma, beta) = <H_C> ──────────────────────────
145+
F = sympy.Integer(0) + const
146+
for i, hi in h.items():
147+
if hi:
148+
F = F + hi * sc.expectation_ps(z=[i])
149+
for (i, j), Jij in J.items():
150+
if Jij:
151+
F = F + Jij * sc.expectation_ps(z=[i, j])
152+
153+
F = sympy.trigsimp(F.rewrite(sympy.cos))
154+
print("Closed-form expectation F(gamma, beta) = <H_C>:")
155+
print(" ", F)
156+
print()
157+
158+
# ── Symbolic gradient ─────────────────────────────────────────────────────
159+
dF_dgamma = sympy.trigsimp(sympy.diff(F, gamma))
160+
dF_dbeta = sympy.trigsimp(sympy.diff(F, beta))
161+
print("dF/dgamma =", dF_dgamma)
162+
print("dF/dbeta =", dF_dbeta)
163+
print()
164+
165+
# ── Numerical cross-validation ────────────────────────────────────────────
166+
print("Numerical cross-validation (symbolic vs tc.Circuit):")
167+
test_points = [(0.5, 0.3), (np.pi / 3, np.pi / 8), (1.0, 0.7)]
168+
for gv, bv in test_points:
169+
sym_val = float(F.subs({gamma: gv, beta: bv}))
170+
c = sc.to_circuit({gamma: gv, beta: bv})
171+
ref_val = float(const)
172+
for i, hi in h.items():
173+
if hi:
174+
ref_val += float(hi) * float(c.expectation_ps(z=[i]).real)
175+
for (i, j), Jij in J.items():
176+
if Jij:
177+
ref_val += float(Jij) * float(c.expectation_ps(z=[i, j]).real)
178+
print(
179+
f" gamma={gv:.4f}, beta={bv:.4f}: "
180+
f"sym={sym_val:.6f} ref={ref_val:.6f} "
181+
f"ok={abs(sym_val - ref_val) < 1e-6}"
182+
)
183+
print()
184+
185+
# ── Landscape: lambdify for fast numpy evaluation ─────────────────────────
186+
F_func = sympy.lambdify([gamma, beta], F, "numpy")
187+
188+
gammas = np.linspace(0, np.pi, 200)
189+
betas = np.linspace(0, np.pi / 2, 200)
190+
GG, BB = np.meshgrid(gammas, betas)
191+
FF = F_func(GG, BB)
192+
193+
# QUBO cost landscape: <f(x)> = -<H_C> (minimisation framing)
194+
FF_loss = -FF
195+
196+
# Optimal parameters (grid-search on QUBO cost → argmin)
197+
idx = np.unravel_index(np.argmin(FF_loss), FF_loss.shape)
198+
g_opt, b_opt, F_opt = GG[idx], BB[idx], FF_loss[idx]
199+
print(
200+
f"Grid-search optimum: gamma*={g_opt:.4f} beta*={b_opt:.4f} "
201+
f"QUBO cost*={F_opt:.4f} (ideal = {-2:.4f})"
202+
)
203+
print()
204+
205+
# ── Plot 2-D energy landscape ─────────────────────────────────────────────
206+
fig, ax = plt.subplots(figsize=(7, 5))
207+
im = ax.pcolormesh(GG, BB, FF_loss, shading="auto", cmap="RdYlGn_r")
208+
cbar = fig.colorbar(im, ax=ax)
209+
cbar.set_label(r"$\langle f(x)\rangle(\gamma,\beta)$ (QUBO cost)", fontsize=12)
210+
211+
ax.scatter(
212+
g_opt,
213+
b_opt,
214+
marker="*",
215+
s=200,
216+
color="white",
217+
zorder=5,
218+
label=f"minimum ({g_opt:.2f}, {b_opt:.2f})",
219+
)
220+
ax.set_xlabel(r"$\gamma$", fontsize=13)
221+
ax.set_ylabel(r"$\beta$", fontsize=13)
222+
ax.set_title(
223+
"1-layer QAOA energy landscape\n"
224+
r"$F(\gamma,\beta)$ from symbolic expression (Max-Cut P3)",
225+
fontsize=12,
226+
)
227+
ax.legend(fontsize=11)
228+
plt.tight_layout()
229+
230+
out = "examples/qaoa_symbolic_landscape.png"
231+
plt.savefig(out, dpi=150)
232+
print(f"Landscape plot saved to {out}")
233+
234+
235+
if __name__ == "__main__":
236+
main()

tensorcircuit/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
from .analogcircuit import AnalogCircuit
3333
from .circuit import Circuit, expectation
3434
from .u1circuit import U1Circuit
35+
36+
try:
37+
from .symbolcircuit import SymbolCircuit
38+
except (ImportError, ModuleNotFoundError):
39+
pass
40+
3541
from .mpscircuit import MPSCircuit
3642
from .densitymatrix import DMCircuit as DMCircuit_reference
3743
from .densitymatrix import DMCircuit2

0 commit comments

Comments
 (0)