Backend Extensibility Example#
This example walks through the backend registry, demonstrates how to discover available backends and register a new one, and shows how capability metadata is used to choose appropriate backends for specific tasks.
What you’ll learn#
How to query the backend registry and read capability flags.
How to register a new backend and make it available to
compile_model.
Source#
1# flake8: noqa
2"""
3Backend extensibility walkthrough (hands-on).
4
5This example is aimed at developers who want to:
6- Understand the compile pipeline (LaTeX → IR → backend).
7- Register a custom backend that uses BaseOperatorCache.
8- Reuse the shared backend registry and parameter validation.
9
10It contains several self-contained steps you can run individually by calling
11the functions from a Python shell. Nothing runs on import.
12"""
13
14from __future__ import annotations
15
16import sys
17from pathlib import Path
18from dataclasses import dataclass
19from typing import Any, Dict
20
21import numpy as np
22
23ROOT = Path(__file__).resolve().parents[1]
24if str(ROOT) not in sys.path:
25 sys.path.insert(0, str(ROOT))
26
27from latex_parser.backend_base import BackendBase, BackendOptions
28from latex_parser.backend_cache import BaseOperatorCache
29from latex_parser.compile_core import (
30 available_backends,
31 compile_model_core,
32 register_backend,
33)
34from latex_parser.dsl import CustomSpec, HilbertConfig, QubitSpec
35from latex_parser.ir import latex_to_ir
36
37
38class NumpyCache(BaseOperatorCache[np.ndarray]):
39 """BaseOperatorCache specialization for NumPy arrays."""
40
41 def _local_identity(self, dim: int) -> np.ndarray:
42 return np.eye(dim)
43
44 def _kron(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
45 return np.kron(a, b)
46
47
48@dataclass
49class NumpyDiagOptions(BackendOptions):
50 """Options for the demo backend."""
51
52 dtype: Any = complex
53
54
55class NumpyDiagBackend(BackendBase):
56 """
57 Diagonal-only backend.
58
59 For each IR term, we evaluate the scalar and place it on the diagonal of a
60 dense NumPy matrix. This is intentionally simple but shows the required
61 BackendBase hooks.
62 """
63
64 def _make_cache(
65 self, config: HilbertConfig, options: BackendOptions | None
66 ) -> NumpyCache:
67 return NumpyCache(config)
68
69 def _compile_static(
70 self,
71 ir,
72 cache: NumpyCache,
73 params: Dict[str, complex],
74 options: BackendOptions | None = None,
75 ) -> np.ndarray:
76 opts = options if isinstance(options, NumpyDiagOptions) else NumpyDiagOptions()
77 diag: list[complex] = []
78 for term in ir.terms:
79 coeff = complex(term.scalar_expr.subs(params))
80 diag.append(coeff)
81 if not diag:
82 diag = [0.0]
83 return np.diag(np.asarray(diag, dtype=opts.dtype))
84
85 def _compile_time_dependent(
86 self,
87 ir,
88 cache: NumpyCache,
89 params: Dict[str, complex],
90 *,
91 t_name: str,
92 time_symbols: tuple[str, ...] | None,
93 options: BackendOptions | None = None,
94 ) -> np.ndarray:
95 # This backend ignores time dependence and just reuses the static path.
96 return self._compile_static(ir, cache, params, options=options)
97
98
99def register_demo_backend() -> None:
100 """
101 Register the diagonal backend under the name "numpy_diag".
102 After calling this, compile_model_core(..., backend="numpy_diag") works.
103 """
104
105 def _compiler(
106 *,
107 H_latex: str,
108 params: Dict[str, complex],
109 config: HilbertConfig,
110 c_ops_latex,
111 t_name: str,
112 time_symbols,
113 ):
114 backend = NumpyDiagBackend()
115 ir = latex_to_ir(H_latex, config, t_name=t_name, time_symbols=time_symbols)
116 cache = backend._make_cache(config, options=None)
117 return backend._compile_time_dependent(
118 ir,
119 cache,
120 params,
121 t_name=t_name,
122 time_symbols=time_symbols,
123 options=None,
124 )
125
126 register_backend("numpy_diag", _compiler)
127
128
129def demo_compile_with_registered_backend() -> None:
130 """
131 Compile a 2x2 diagonal Hamiltonian with the registered backend.
132 """
133 if "numpy_diag" not in available_backends():
134 register_demo_backend()
135
136 cfg = HilbertConfig(qubits=[QubitSpec(label="q", index=1)], bosons=[], customs=[])
137 H_latex = r"\Delta \sigma_{z,1}"
138 params = {"Delta": 1.25}
139 H_np = compile_model_core(
140 backend="numpy_diag",
141 H_latex=H_latex,
142 params=params,
143 config=cfg,
144 c_ops_latex=None,
145 t_name="t",
146 time_symbols=None,
147 )
148 print("Available backends:", available_backends())
149 print("Diagonal matrix:\n", H_np)
150
151
152def demo_custom_subsystem() -> None:
153 """
154 Use the diagonal backend with a custom subsystem (Jx/Jy/Jz).
155 """
156 if "numpy_diag" not in available_backends():
157 register_demo_backend()
158
159 Jx = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex)
160 Jy = np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex)
161 Jz = np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex)
162
163 custom = CustomSpec(
164 label="c",
165 index=1,
166 dim=2,
167 operators={"Jx": Jx, "Jy": Jy, "Jz": Jz},
168 )
169 cfg = HilbertConfig(qubits=[], bosons=[], customs=[custom])
170 H_latex = r"\alpha Jx_{1} + \beta Jy_{1} + \gamma Jz_{1}"
171 params = {"alpha": 0.1, "beta": 0.2, "gamma": 0.3}
172 H_np = compile_model_core(
173 backend="numpy_diag",
174 H_latex=H_latex,
175 params=params,
176 config=cfg,
177 c_ops_latex=None,
178 t_name="t",
179 time_symbols=None,
180 )
181 print("Custom subsystem diagonal matrix:\n", H_np)
182
183
184if __name__ == "__main__":
185 register_demo_backend()
186 demo_compile_with_registered_backend()
187 demo_custom_subsystem()
Run#
python examples/example_backend_extensibility.py
Notes#
Use this example to scaffold integrations with specialized numerical libraries.