SCQubits Example#
This example demonstrates interoperability with the scqubits package (if available) and shows how to compose models that include non-standard qubit implementations.
What you’ll learn#
How to adapt latex_parser outputs to other quantum libraries.
How to inspect and adapt operator shapes when integrating with third-party packages.
Source#
1#!/usr/bin/env python
2# flake8: noqa
3"""
4Examples: integrating scqubits transmon/fluxonium with the LaTeX DSL.
5
6These examples show how to:
71) Build scqubits devices.
82) Register their operators as CustomSpec entries.
93) Use operator-valued functions like cos(phi) directly in the DSL.
104) Verify compiled Hamiltonians against scqubits-provided matrices.
11
12No long simulations are run by default; the examples stop after compiling
13to QuTiP objects and, optionally, validating against scqubits to keep
14runtime light. Run this file directly to execute both examples if
15scqubits is installed.
16"""
17
18from __future__ import annotations
19
20import sys
21from pathlib import Path
22import logging
23from typing import Tuple
24
25import numpy as np
26from qutip import Qobj, destroy, qeye, tensor # type: ignore
27from scipy.linalg import cosm, sinm # type: ignore
28
29ROOT = Path(__file__).resolve().parents[1]
30if str(ROOT) not in sys.path:
31 sys.path.insert(0, str(ROOT))
32
33from latex_parser.dsl import CustomSpec, HilbertConfig
34from latex_parser.latex_api import compile_model as compile_latex_model, make_config
35
36logger = logging.getLogger(__name__)
37
38
39def _require_scqubits():
40 try:
41 import scqubits as scq # type: ignore
42 except ImportError as exc: # pragma: no cover - optional dependency
43 raise RuntimeError(
44 "scqubits is required for these examples. Install it with "
45 "'pip install scqubits'."
46 ) from exc
47 return scq
48
49
50def fluxonium_model(
51 EJ: float = 8.0,
52 EC: float = 1.0,
53 EL: float = 0.5,
54 cutoff: int = 35,
55 n_g: float = 0.0,
56 flux: float = 0.0,
57) -> Tuple[object, HilbertConfig]:
58 """
59 Build a fluxonium Hamiltonian
60
61 H = 4 EC (n - n_g)^2 + 0.5 EL phi^2 - EJ cos(phi)
62
63 using scqubits operators mapped into a CustomSpec. Returns the compiled
64 model and the HilbertConfig.
65 """
66 scq = _require_scqubits()
67
68 flux_dev = scq.Fluxonium(EJ=EJ, EC=EC, EL=EL, cutoff=cutoff, flux=flux)
69 phi_op = flux_dev.phi_operator()
70 n_op = flux_dev.n_operator()
71
72 # scqubits >=3 returns NumPy arrays; wrap into Qobj for the DSL.
73 if not hasattr(phi_op, "dims"):
74 phi_op = Qobj(phi_op, dims=[[cutoff], [cutoff]])
75 if not hasattr(n_op, "dims"):
76 n_op = Qobj(n_op, dims=[[cutoff], [cutoff]])
77
78 # Register custom subsystem with phi and n operators.
79 spec = CustomSpec(
80 label="c",
81 index=1,
82 dim=phi_op.dims[0][0],
83 operators={"phi": phi_op, "nphi": n_op},
84 )
85 cfg = HilbertConfig(qubits=[], bosons=[], customs=[spec])
86
87 H_latex = r"4 EC (nphi_{1} - n_g)^2 + \frac{1}{2} EL \phi_{1}^2 - EJ \cos(\phi_{1})"
88 params = {"EC": EC, "EL": EL, "EJ": EJ, "n_g": n_g}
89
90 model = compile_latex_model(
91 H_latex=H_latex,
92 params=params,
93 config=cfg,
94 t_name="t",
95 )
96 return model, cfg
97
98
99def fluxonium_model_with_flux_offset(
100 EJ: float = 8.0,
101 EC: float = 1.0,
102 EL: float = 0.5,
103 cutoff: int = 35,
104 n_g: float = 0.0,
105 phi_ext: float = 0.2,
106) -> Tuple[object, HilbertConfig]:
107 """
108 Fluxonium with external flux offset via the shifted cosine
109
110 -EJ cos(phi - phi_ext) = -EJ [cos(phi) cos(phi_ext) + sin(phi) sin(phi_ext)].
111
112 Uses operator-valued cos/sin on the phi operator and scalar parameters
113 for phi_ext.
114 """
115 scq = _require_scqubits()
116
117 flux_dev = scq.Fluxonium(EJ=EJ, EC=EC, EL=EL, cutoff=cutoff, flux=phi_ext)
118 phi_op = flux_dev.phi_operator()
119 n_op = flux_dev.n_operator()
120
121 if not hasattr(phi_op, "dims"):
122 phi_op = Qobj(phi_op, dims=[[cutoff], [cutoff]])
123 if not hasattr(n_op, "dims"):
124 n_op = Qobj(n_op, dims=[[cutoff], [cutoff]])
125
126 spec = CustomSpec(
127 label="c",
128 index=1,
129 dim=phi_op.dims[0][0],
130 operators={"phi": phi_op, "nphi": n_op},
131 )
132 cfg = HilbertConfig(qubits=[], bosons=[], customs=[spec])
133
134 H_latex = r"""
135 4 EC (nphi_{1} - n_g)^2
136 + \frac{1}{2} EL \phi_{1}^2
137 - EJ \left(
138 \cos(\phi_{1}) \cos(\phi_{ex})
139 + \sin(\phi_{1}) \sin(\phi_{ex})
140 \right)
141 """
142 params = {
143 "EC": EC,
144 "EL": EL,
145 "EJ": EJ,
146 "n_g": n_g,
147 "phi_ex": phi_ext,
148 }
149
150 model = compile_latex_model(
151 H_latex=H_latex,
152 params=params,
153 config=cfg,
154 t_name="t",
155 )
156 return model, cfg
157
158
159def verify_fluxonium_against_scqubits(
160 EJ: float = 8.0,
161 EC: float = 1.0,
162 EL: float = 0.5,
163 cutoff: int = 15,
164 n_g: float = 0.0,
165 flux: float = 0.0,
166) -> float:
167 """
168 Compile the fluxonium Hamiltonian from LaTeX and compare to an explicit
169 matrix built from the same scqubits ``phi`` and ``n`` operators. Returns
170 the operator norm of the difference (expected to be ~0).
171 """
172 scq = _require_scqubits()
173 model, cfg = fluxonium_model(EJ=EJ, EC=EC, EL=EL, cutoff=cutoff, n_g=n_g, flux=flux)
174 H_dsl = model.H if isinstance(model.H, Qobj) else model.H[0]
175 if not isinstance(H_dsl, Qobj) and hasattr(H_dsl, "shape"):
176 dim = H_dsl.shape[0]
177 H_dsl = Qobj(H_dsl, dims=[[dim], [dim]])
178
179 flux_dev = scq.Fluxonium(EJ=EJ, EC=EC, EL=EL, cutoff=cutoff, flux=flux)
180 phi = flux_dev.phi_operator()
181 n_op = flux_dev.n_operator()
182 if not hasattr(phi, "dims"):
183 phi = Qobj(phi, dims=[[cutoff], [cutoff]])
184 if not hasattr(n_op, "dims"):
185 n_op = Qobj(n_op, dims=[[cutoff], [cutoff]])
186
187 cos_phi = Qobj(cosm(phi.full()), dims=phi.dims)
188 H_ref = 4 * EC * (n_op - n_g) ** 2 + 0.5 * EL * (phi**2) - EJ * cos_phi
189
190 H_sq = flux_dev.hamiltonian()
191 if not isinstance(H_sq, Qobj) and hasattr(H_sq, "shape"):
192 dim = H_sq.shape[0]
193 H_sq = Qobj(H_sq, dims=[[dim], [dim]])
194
195 diff = (H_dsl - H_ref).norm()
196 diff_sq = (H_ref - H_sq).norm()
197 if diff_sq > 0: # pragma: no cover - informational
198 logger.info(
199 "scqubits Hamiltonian differs from reference by %.3e (truncation effects?)",
200 diff_sq,
201 )
202 return diff
203
204
205def verify_fluxonium_with_flux_offset_against_scqubits(
206 EJ: float = 8.0,
207 EC: float = 1.0,
208 EL: float = 0.5,
209 cutoff: int = 15,
210 n_g: float = 0.0,
211 phi_ext: float = 0.2,
212) -> float:
213 """
214 Verify flux-offset fluxonium by comparing the DSL-compiled Hamiltonian
215 to an explicit reference built from scqubits ``phi``/``n`` operators and
216 matrix cosine/sine. Returns the operator norm of the difference
217 (expected ~0).
218 """
219 scq = _require_scqubits()
220 model, cfg = fluxonium_model_with_flux_offset(
221 EJ=EJ, EC=EC, EL=EL, cutoff=cutoff, n_g=n_g, phi_ext=phi_ext
222 )
223 H_dsl = model.H if isinstance(model.H, Qobj) else model.H[0]
224 if not isinstance(H_dsl, Qobj) and hasattr(H_dsl, "shape"):
225 dim = H_dsl.shape[0]
226 H_dsl = Qobj(H_dsl, dims=[[dim], [dim]])
227
228 flux_dev = scq.Fluxonium(EJ=EJ, EC=EC, EL=EL, cutoff=cutoff, flux=phi_ext)
229 phi = flux_dev.phi_operator()
230 n_op = flux_dev.n_operator()
231 if not hasattr(phi, "dims"):
232 phi = Qobj(phi, dims=[[cutoff], [cutoff]])
233 if not hasattr(n_op, "dims"):
234 n_op = Qobj(n_op, dims=[[cutoff], [cutoff]])
235
236 cos_phi = Qobj(cosm(phi.full()), dims=phi.dims)
237 sin_phi = Qobj(sinm(phi.full()), dims=phi.dims)
238 H_ref = (
239 4 * EC * (n_op - n_g) ** 2
240 + 0.5 * EL * (phi**2)
241 - EJ * (cos_phi * np.cos(phi_ext) + sin_phi * np.sin(phi_ext))
242 )
243
244 H_sq = flux_dev.hamiltonian()
245 if not isinstance(H_sq, Qobj) and hasattr(H_sq, "shape"):
246 dim = H_sq.shape[0]
247 H_sq = Qobj(H_sq, dims=[[dim], [dim]])
248
249 diff = (H_dsl - H_ref).norm()
250 diff_sq = (H_ref - H_sq).norm()
251 if diff_sq > 0: # pragma: no cover - informational
252 logger.info(
253 "scqubits Hamiltonian with flux offset differs from reference by %.3e",
254 diff_sq,
255 )
256 return diff
257
258
259def transmon_cavity_model(
260 EJ: float = 20.0,
261 EC: float = 0.25,
262 cutoff: int = 10,
263 cav_cutoff: int = 6,
264 g: float = 0.05,
265) -> Tuple[object, HilbertConfig]:
266 r"""
267 Build a simple transmon + cavity charge-coupled Hamiltonian:
268
269 H = H_transmon + omega_c n_cav + g n (a + a^\dagger)
270
271 where H_transmon = 4 EC n^2 - EJ cos(phi). The transmon operators are
272 built internally using harmonic-oscillator zero-point fluctuations
273 (no scqubits operators are used in the compiled model); the cavity
274 is a boson in the DSL.
275 """
276 H_tmon, phi_op, n_op = _build_transmon_ops_internal(EJ=EJ, EC=EC, cutoff=cutoff)
277
278 spec = CustomSpec(
279 label="c",
280 index=1,
281 dim=H_tmon.dims[0][0],
282 operators={"Htm": H_tmon, "phi": phi_op, "ntm": n_op},
283 )
284 # One cavity boson a_1 with cutoff cav_cutoff
285 cfg = make_config(qubits=0, bosons=[(cav_cutoff, "a")], customs=[spec])
286
287 H_latex = r"Htm_{1} + \omega_c n_{1} + g \, ntm_{1} (a_{1} + a_{1}^{\dagger})"
288 params = {"omega_c": 5.0, "g": g}
289
290 model = compile_latex_model(
291 H_latex=H_latex,
292 params=params,
293 config=cfg,
294 t_name="t",
295 )
296 return model, cfg
297
298
299def verify_transmon_cavity_against_scqubits(
300 EJ: float = 20.0,
301 EC: float = 0.25,
302 cutoff: int = 8,
303 cav_cutoff: int = 4,
304 g: float = 0.05,
305 omega_c: float = 5.0,
306) -> float:
307 """
308 Compile the transmon-cavity Hamiltonian from LaTeX and compare to an
309 explicit construction built from the same internal transmon operators
310 used in :func:`transmon_cavity_model`. Returns the operator norm of the
311 difference (expected ~0). Any deviation between this reference and
312 scqubits' transmon Hamiltonian is logged for information.
313 """
314 scq = _require_scqubits()
315 model, cfg = transmon_cavity_model(
316 EJ=EJ, EC=EC, cutoff=cutoff, cav_cutoff=cav_cutoff, g=g
317 )
318 H_dsl: Qobj = model.H if isinstance(model.H, Qobj) else model.H[0]
319 H_tm, _, n_tm = _build_transmon_ops_internal(EJ=EJ, EC=EC, cutoff=cutoff)
320
321 # Cavity operators (boson is first subsystem in cfg ordering, custom next)
322 a = destroy(cav_cutoff)
323 n_cav = a.dag() * a
324
325 # Tensor order: [boson, custom]
326 I_tm = qeye(H_tm.dims[0])
327 I_cav = qeye([cav_cutoff])
328
329 H_cav = omega_c * tensor(n_cav, I_tm)
330 H_tm_full = tensor(I_cav, H_tm)
331 n_tm_full = tensor(I_cav, n_tm)
332 a_full = tensor(a, I_tm)
333 adag_full = tensor(a.dag(), I_tm)
334
335 H_expected = H_tm_full + H_cav + g * n_tm_full * (a_full + adag_full)
336 diff = (H_dsl - H_expected).norm()
337
338 # Informational check against scqubits' transmon implementation
339 try: # pragma: no cover - optional dependency precision differences
340 tmon = scq.Transmon(EJ=EJ, EC=EC, ng=0.0, ncut=cutoff, truncated_dim=cutoff)
341 H_tm_scq = tmon.hamiltonian()
342 n_tm_scq = tmon.n_operator()
343 if not hasattr(H_tm_scq, "dims"):
344 dim_tm = H_tm_scq.shape[0]
345 H_tm_scq = Qobj(H_tm_scq, dims=[[dim_tm], [dim_tm]])
346 if not hasattr(n_tm_scq, "dims"):
347 dim_tm = n_tm_scq.shape[0]
348 n_tm_scq = Qobj(n_tm_scq, dims=[[dim_tm], [dim_tm]])
349
350 H_tm_full_scq = tensor(I_cav, H_tm_scq)
351 n_tm_full_scq = tensor(I_cav, n_tm_scq)
352 H_expected_scq = (
353 H_tm_full_scq + H_cav + g * n_tm_full_scq * (a_full + adag_full)
354 )
355 diff_scq = (H_expected - H_expected_scq).norm()
356 if diff_scq > 0:
357 logger.info(
358 "Transmon reference differs from scqubits by %.3e "
359 "(basis/truncation effects).",
360 diff_scq,
361 )
362 except Exception:
363 pass
364
365 return float(diff)
366
367
368def _build_transmon_ops_internal(
369 EJ: float, EC: float, cutoff: int
370) -> Tuple[Qobj, Qobj, Qobj]:
371 """
372 Build transmon phi, n, and Hamiltonian using harmonic-oscillator
373 zero-point fluctuations (Koch et al.).
374 """
375 dim = 2 * cutoff + 1 # match scqubits Transmon dimension (2*ncut + 1)
376 a = destroy(dim)
377 adag = a.dag()
378 # Zero-point fluctuations
379 phi_zpf = (2 * EC / EJ) ** 0.25
380 n_zpf = (EJ / (32 * EC)) ** 0.25
381
382 phi_op = phi_zpf * (a + adag)
383 n_op = 1j * n_zpf * (adag - a)
384
385 # cos(phi) via matrix cosine
386 cos_phi = Qobj(cosm(phi_op.full()), dims=phi_op.dims)
387 H_tm = 4 * EC * (n_op * n_op) - EJ * cos_phi
388 return H_tm, phi_op, n_op
389
390
391if __name__ == "__main__": # pragma: no cover - example execution
392 try:
393 flux_model, flux_cfg = fluxonium_model()
394 print("Fluxonium model compiled. time_dependent:", flux_model.time_dependent)
395 diff_flux = verify_fluxonium_against_scqubits()
396 print(f"Fluxonium H difference norm (vs reference): {diff_flux:.3e}")
397
398 flux_model_off, _ = fluxonium_model_with_flux_offset()
399 diff_flux_off = verify_fluxonium_with_flux_offset_against_scqubits()
400 print(
401 "Fluxonium (flux offset) H difference norm (vs reference): "
402 f"{diff_flux_off:.3e}"
403 )
404 except RuntimeError as exc:
405 print(exc)
406
407 try:
408 tmon_model, tmon_cfg = transmon_cavity_model()
409 print(
410 "Transmon-cavity model compiled. time_dependent:",
411 tmon_model.time_dependent,
412 )
413 diff_tmon = verify_transmon_cavity_against_scqubits()
414 print(
415 f"Transmon-cavity H difference norm: {diff_tmon:.3e}",
416 )
417 except RuntimeError as exc:
418 print(exc)
Run#
python examples/example_scqubits.py
Notes#
This example may require scqubits to be installed; it demonstrates integration patterns rather than a turnkey workflow.