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.