Custom Backend (Minimal) Example

Custom Backend (Minimal) Example#

This example provides a minimal custom backend implementation that reuses the shared caching utilities. It’s a good template for implementing new numerical backends or connecting to external libraries.

What you’ll learn#

  • Which hooks a backend must implement (cache creation and compile methods).

  • How to register a backend with register_backend.

Source#

 1# flake8: noqa
 2"""
 3Example: defining a custom backend by subclassing BackendBase.
 4
 5This backend uses NumPy to build simple diagonal matrices from IR terms,
 6and demonstrates how to register a custom LaTeX macro and operator function.
 7
 8It also shows how to reuse `BaseOperatorCache` for subsystem bookkeeping,
 9so backend authors can avoid duplicating identity/kron logic.
10"""
11
12from __future__ import annotations
13
14import sys
15from pathlib import Path
16
17import numpy as np
18
19ROOT = Path(__file__).resolve().parents[1]
20if str(ROOT) not in sys.path:
21    sys.path.insert(0, str(ROOT))
22
23from latex_parser.backend_base import BackendBase, BackendOptions
24from latex_parser.backend_cache import BaseOperatorCache
25from latex_parser.dsl import CustomSpec, HilbertConfig, register_operator_macro
26from latex_parser.dsl_constants import register_operator_function
27from latex_parser.ir import latex_to_ir
28
29
30class NumpyOperatorCache(BaseOperatorCache[np.ndarray]):
31    """Minimal cache that builds identity tensors for NumPy backends."""
32
33    def _local_identity(self, dim: int) -> np.ndarray:
34        return np.eye(dim)
35
36    def _kron(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
37        return np.kron(a, b)
38
39
40class NumpyDiagBackend(BackendBase):
41    def _make_cache(self, config: HilbertConfig, options: BackendOptions | None):
42        return NumpyOperatorCache(config)
43
44    def _compile_static(self, ir, cache, params, options=None):
45        diag = []
46        for term in ir.terms:
47            coeff = complex(term.scalar_expr.subs(params))
48            diag.append(coeff)
49        if not diag:
50            diag = [0.0]
51        return np.diag(diag)
52
53    def _compile_time_dependent(
54        self, ir, cache, params, *, t_name, time_symbols, options=None
55    ):
56        return self._compile_static(ir, cache, params, options=options)
57
58
59if __name__ == "__main__":
60    # Register a simple macro foo_j -> Jx_j and allow sinh
61    register_operator_macro("foo", "Jx")
62    register_operator_function("sinh")
63
64    # Custom subsystem with Jx operator
65    Jx = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex)
66    custom = CustomSpec(label="c", index=1, dim=2, operators={"Jx": Jx})
67    cfg = HilbertConfig(qubits=[], bosons=[], customs=[custom])
68
69    ir = latex_to_ir(r"\sinh(\foo_{1})", cfg)
70    backend = NumpyDiagBackend()
71    H = backend.compile_static_from_ir(ir, cfg, params={})
72    print("Compiled Hamiltonian (diagonal):\n", H)

Run#

python examples/custom_backend.py

Notes#

  • This example is intentionally minimal. Use it as the basis for a production backend by replacing the identity and kron implementations with optimized primitives.