Backend Extensibility Example

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.