Usage Guide#
Quick Start (5 Minutes)#
Let’s compile your first quantum model from LaTeX in 5 minutes:
from latex_parser.latex_api import compile_model
# 1. Define your Hamiltonian in LaTeX (as you would in a paper)
H_latex = r"\frac{\omega_0}{2} \sigma_z + A \cos(\omega t) \sigma_x"
# 2. List your parameters
params = {
"omega_0": 2.0, # Transition frequency
"A": 0.3, # Drive amplitude
"omega": 1.5, # Drive frequency
}
# 3. Compile to your chosen backend (QuTiP by default)
model = compile_model(
H_latex=H_latex,
params=params,
qubits=1, # One qubit
backend="qutip", # or "numpy", "jax"
)
# 4. Use the result
print(f"Hamiltonian: {model.H}")
print(f"Type: {type(model.H)}") # QuTiP Qobj
# For time-dependent models, model.args contains parameter dict
What just happened?
Your LaTeX was canonicalized —
\sigma_zand other physics notation was converted to internal macros.The DSL was validated — operators, functions, and syntax were checked.
An Intermediate Representation (IR) was built — the model was decomposed into
(scalar_expr, operator_term)pairs.Parameters were validated — all required symbols (
omega_0,A,omega) were found and substituted.The backend compiled the IR to a
Qobj(QuTiP), ndarray (NumPy), or JAX function.
All in one line of Python! 🎉
Understanding the Compilation Pipeline#
The LaTeX → IR → Backend pipeline has five distinct stages. Understanding each helps with debugging and extension:
┌─────────────────────────────────────────────────────────────┐
│ Stage 1: Canonicalize LaTeX │
│ Rewrite: a_{j}^† → \mop_adjoint{a}{j} │
│ σ_{x,j} → \mop_sigma_x{j} │
│ Ω_c → \Omega_{c} │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Stage 2: DSL Validation & Parsing │
│ Check: Allowed operators, functions, subsystem definitions │
│ Parse: Split into terms, extract operators & scalars │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Stage 3: Build Intermediate Representation (IR) │
│ Output: List of (SymPy_scalar_expr, OperatorTerm) pairs │
│ Time-dependence detected from free symbols in │
│ scalar expressions (look for t_name or time_symbols)│
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Stage 4: Validate & Collect Parameters │
│ Find: All free symbols in scalar expressions │
│ Resolve: Aliases (ω_c ↔ omega_c ↔ omega c) │
│ Validate: All required params are present │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Stage 5: Backend Dispatch & Compilation │
│ Route: To QuTiP, NumPy, JAX, or custom backend │
│ Compile: Each backend builds operators from IR using shared │
│ operator cache (BaseOperatorCache) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Output: Backend-specific object │
│ QuTiP: Qobj (or list of Qobj) │
│ NumPy: ndarray │
│ JAX: Parametrized function │
│ Custom: Your type │
└─────────────────────────────────────────────────────────────┘
Key insight: The IR is transparent and inspectable. If something goes wrong, you can examine each stage independently. See Examples for IR debugging examples.
Parameter Handling & Aliases#
LaTeX has many ways to write the same symbol. latex_parser normalizes them automatically:
Alias equivalence rules:
\omega_c,\omega_{c},omega_c,omega_{c},\omega c,omega c→ all refer to the same parameterUnderscores, braces, and spaces are ignored
Case-sensitive:
omega≠Omega
Example:
H_latex = r"\omega_{c} \sigma_z + \Omega_R \sigma_x"
# These all work:
params_a = {"omega_c": 1.0, "Omega_R": 0.5}
params_b = {"omega c": 1.0, "Omega R": 0.5}
params_c = {r"\omega_{c}": 1.0, r"\Omega_R": 0.5}
# All produce the same result!
model_a = compile_model(H_latex=H_latex, params=params_a, qubits=1)
model_b = compile_model(H_latex=H_latex, params=params_b, qubits=1)
model_c = compile_model(H_latex=H_latex, params=params_c, qubits=1)
What counts as a parameter?
Scalars — Free symbols in scalar expressions (
\omega,A, etc.)NOT operators —
\sigma_z,a^\daggerare not parametersNOT constants —
\pi,e, integers (2,\frac{1}{2}) are pre-defined
Validation happens early:
If you forget a parameter, you get a clear error:
H_latex = r"\omega_0 \sigma_z + \Omega \sigma_x"
params = {"omega_0": 1.0} # Forgot Omega!
try:
model = compile_model(H_latex=H_latex, params=params, qubits=1)
except DSLValidationError as e:
print(e)
# Output: "Missing required parameter: Omega (in \Omega \sigma_x)"
Time-Dependent Hamiltonians#
latex_parser automatically detects time dependence in scalar expressions:
Static Hamiltonian:
H_latex = r"\omega_0 \sigma_z + A \sigma_x"
model = compile_model(H_latex=H_latex, params={"omega_0": 1.0, "A": 0.5}, qubits=1)
print(type(model.H)) # <class 'qutip.Qobj'> — a single static matrix
Time-Dependent Hamiltonian (cosine envelope):
H_latex = r"\omega_0 \sigma_z + A \cos(\omega t) \sigma_x"
model = compile_model(
H_latex=H_latex,
params={"omega_0": 1.0, "A": 0.5, "omega": 2.0},
t_name="t", # Tell the parser what your time variable is called
qubits=1
)
print(type(model.H)) # <class 'list'> — list of [H0, [H1, args_dict]]
# Ready for mesolve with time-dependent Hamiltonian!
How does time-dependence detection work?
After building the IR, the parser looks at free symbols in each scalar expression
If the time variable (
t_name, default ist) appears, that term is time-dependentStatic and time-dependent terms are separated and returned in QuTiP format
Supported time dependencies:
\cos(\omega t),\sin(\omega t)— oscillating envelopes\exp(-\gamma t)— exponential decay\sqrt{t}— smooth turn-on functionsAny SymPy expression in
tis supported
Collapse Operators#
For open quantum systems, specify collapse operators (Lindblad operators):
H_latex = r"\omega_0 \sigma_z + A \sigma_x"
c_ops_latex = [
r"\sqrt{\gamma_1} \sigma_{-}", # Decay from excited to ground
r"\sqrt{\gamma_2} (\sigma_x + i\sigma_y)/\sqrt{2}", # Dephasing
]
model = compile_model(
H_latex=H_latex,
c_ops_latex=c_ops_latex,
params={
"omega_0": 1.0, "A": 0.5,
"gamma_1": 0.01, # Decay rate
"gamma_2": 0.005, # Dephasing rate
},
qubits=1,
backend="qutip", # Required for collapse operators
)
print(model.c_ops) # List of Qobj collapse operators
Important: Collapse operators must contain at least one operator term. Scalar-only collapse strings are rejected.
Backend Selection#
Choose the right backend for your task:
- QuTiP (Default)
Best for: Master equations, time evolution, expectation values, measurements.
model = compile_model(H_latex=H, params=p, qubits=1, backend="qutip") # Returns: QuTiP Qobj or list [H0, [H1, args_dict]] # Use with: qutip.mesolve, mcsolve, eigenenergies, etc.
- NumPy
Best for: Dense matrices, testing, educational use, checking results.
model = compile_model(H_latex=H, params=p, qubits=1, backend="numpy") # Returns: numpy.ndarray (dense, potentially large) # Use with: numpy.linalg, scipy.sparse, etc.
- JAX
Best for: Automatic differentiation, GPU compilation, parameter optimization, batching.
model = compile_model(H_latex=H, params=p, qubits=1, backend="jax") # Returns: JAX-compilable function (scalable to larger systems) # Use with: jax.grad, jax.vmap, jax.jit, etc. import jax def loss(params): H = model.evaluate(params) eigenvalues = jax.numpy.linalg.eigvalsh(H) return eigenvalues[0] # Ground state energy grad_fn = jax.grad(loss) grads = grad_fn({"omega_0": 1.0, ...})
- Custom Backend
Best for: Integration with other frameworks, specialized computations.
See the
examples/custom_backend.pyfor a minimal template usingBaseOperatorCache.
Subsystems: Qubits, Bosons, Custom#
Define your system’s structure:
Qubits only:
model = compile_model(
H_latex=r"\sigma_z",
params={},
qubits=2, # Two qubits: indices 0, 1
)
Bosons (with Hilbert cutoff):
model = compile_model(
H_latex=r"\omega a^\dagger a",
params={"omega": 1.0},
bosons=[(0, 2)], # Boson at mode 0, cutoff dimension 2
)
Mixed qubit-boson system:
model = compile_model(
H_latex=r"\omega \sigma_z + g (\sigma_+ a + \sigma_- a^\dagger)",
params={"omega": 1.0, "g": 0.1},
qubits=1, # One qubit
bosons=[(0, 4)], # One boson, cutoff 4
)
Custom subsystems:
from latex_parser.dsl import CustomSpec
# Define a spin-3/2 system (dimension 4)
spin32_spec = CustomSpec(
label="spin32",
index=0,
dim=4,
# Custom algebra can be added via register_operator_function
)
model = compile_model(
H_latex=r"\sigma_z", # Uses your custom algebra
params={},
customs=[spin32_spec],
)
See examples/example_custom_subsystem.py for a complete walkthrough.
Backend Registry#
The backend registry lets you register your own backends and discover available ones:
Using the registry:
from latex_parser.compile_core import register_backend, get_registered_backends
# List available backends
backends = get_registered_backends()
print(backends) # ["qutip", "numpy", "jax", ...]
# Check if a backend is available
if "mybackend" in backends:
model = compile_model(H_latex=H, params=p, backend="mybackend")
Registering a custom backend:
from latex_parser.compile_core import register_backend
def my_backend_fn(H_latex, params, config, c_ops_latex=None, t_name="t", time_symbols=None, **kwargs):
"""Your backend compilation logic."""
# 1. Parse LaTeX → IR
from latex_parser.ir import latex_to_ir
ir = latex_to_ir(H_latex, config, t_name, time_symbols)
# 2. Compile IR to your backend
my_result = compile_my_backend(ir, params, config)
# 3. Return backend-specific object
return my_result
# Register with the system
register_backend("mybackend", my_backend_fn)
# Now use it
model = compile_model(H_latex=H, params=p, backend="mybackend")
See examples/custom_backend.py and examples/example_backend_extensibility.py for complete examples.
Extending the DSL#
Customize LaTeX patterns, operator functions, and more:
Register custom LaTeX patterns:
from latex_parser.dsl import register_latex_pattern
# Example: Map \mathcal{L} to a custom operator
register_latex_pattern(r"\mathcal{L}", "custom_L_operator")
H_latex = r"\mathcal{L}"
# Now your pattern is recognized
Register custom operator functions:
from latex_parser.dsl_constants import register_operator_function
# Add support for \sinh (hyperbolic sine of an operator)
# Allowed: exp, cos, sin, cosh, sinh, sqrtm (square root of matrix)
register_operator_function("sinh", "sinh_of_operator")
Add custom subsystems:
See the subsystems section above and examples/example_custom_subsystem.py.
Common Pitfalls & Troubleshooting#
Pitfall 1: Collapse operator is scalar-only
# ❌ Wrong: c_ops_latex = [r"\gamma"] # Just a number!
# ✅ Right: c_ops_latex = [r"\sqrt{\gamma} \sigma_{-}"] # Number × operator
Pitfall 2: Forgetting a parameter
H_latex = r"\omega_0 \sigma_z"
params = {} # Forgot omega_0!
# → DSLValidationError: Missing required parameter: omega_0
# Fix: params = {"omega_0": 1.0}
Pitfall 3: Inconsistent alias naming
H_latex = r"\omega_{c} \sigma_z"
params = {"omega_C": 1.0} # Case mismatch (c vs C)
# → DSLValidationError: Missing required parameter: omega_c
# Fix: Use consistent case or provide all variants
Pitfall 4: Huge expansion from high powers
H_latex = r"(a_1 + a_2 + a_3 + a_4)^{10}" # Expands to ~1000 terms
# → May hit MAX_EXPANDED_TERMS guard (default: 10000)
# Fix: Simplify or use symbolic expansions where possible
Pitfall 5: JAX backend requires JAX installed
# pip install jax jaxlib first!
model = compile_model(H_latex=H, params=p, backend="jax")
# → ImportError if jax not found
Pitfall 6: Time-dependence with wrong variable name
H_latex = r"\Omega \cos(t)"
# By default, t_name="t", so this is detected as time-dependent ✅
# But:
H_latex = r"\Omega \cos(\tau)"
model = compile_model(H_latex=H_latex, params={}, qubits=1)
# ❌ Treats \tau as a parameter, not a variable → Error: missing parameter
# Fix: Specify t_name="tau"
model = compile_model(H_latex=H_latex, params={}, qubits=1, t_name="tau")
Debugging & Inspection#
Inspect the IR (Intermediate Representation):
from latex_parser.ir import latex_to_ir
from latex_parser.dsl import HilbertConfig, QubitSpec
H_latex = r"\omega_0 \sigma_z + A \cos(\omega t) \sigma_x"
config = HilbertConfig(qubits=[QubitSpec(label="q", index=0)])
ir = latex_to_ir(H_latex, config, t_name="t")
# Inspect the IR
print(f"Terms: {len(ir.terms)}")
for i, term in enumerate(ir.terms):
print(f"Term {i}: scalar={term.scalar_expr}, ops={term.ops}")
print(f"Time-dependent: {ir.has_time_dep}")
print(f"Free symbols: {ir.free_symbols}")
Collect parameters without compiling:
from latex_parser.ir import latex_to_ir
ir = latex_to_ir(H_latex, config, t_name="t")
required_params = ir.free_symbols
print(f"Required parameters: {required_params}")
Check what your backend received:
# Enable logging
import logging
logging.basicConfig(level=logging.DEBUG)
model = compile_model(H_latex=H, params=p, qubits=1)
# Now you'll see detailed traces of each compilation stage
See examples/example_ir_debugging.py for a complete debugging walkthrough.
Backend Features & Maturity#
Different backends support different features. Choose based on your use case:
QuTiP Backend (Recommended for open systems)
Feature |
Support |
Notes |
|---|---|---|
Static Hamiltonians |
✅ Mature |
Fully optimized, production-ready |
Time-dependent Hamiltonians |
✅ Mature |
|
Collapse operators (static) |
✅ Mature |
Optimized for Lindblad simulation |
Collapse operators (time-dependent) |
✅ Mature |
Full support; QuTiP-only feature |
Open-system solvers |
✅ Mature |
|
Automatic differentiation |
⚠️ Limited |
Manual diff only; use JAX for autodiff |
Custom backends |
✅ Easy |
Subclass |
Debugging |
✅ Excellent |
Full IR inspection, easy troubleshooting |
- Use QuTiP when:
Simulating open systems (dissipation, decay)
You need collapse operators (especially time-dependent)
Speed and stability are priorities
You want built-in solvers
JAX Backend (For automatic differentiation)
Feature |
Support |
Notes |
|---|---|---|
Static Hamiltonians |
✅ Mature |
JAX arrays, fast and differentiable |
Time-dependent Hamiltonians |
✅ Mature |
Parametrized functions, easy grad/vmap |
Collapse operators (static) |
⚠️ Limited |
Returns array, not integrated solver |
Collapse operators (time-dependent) |
❌ Not supported |
Use custom solver or QuTiP |
Open-system solvers |
❌ Not included |
JAX includes only Hamiltonian compilation |
Automatic differentiation |
✅ Mature |
Full support: |
Custom backends |
✅ Easy |
Subclass |
Debugging |
✅ Good |
IR inspection works; JAX tracing may limit prints |
- Use JAX when:
Optimizing Hamiltonian parameters (pulse sequences, gate fidelities)
You need automatic differentiation (gradient-based optimization)
GPU acceleration is required
Closed-system simulations only
NumPy Backend (For prototyping)
Feature |
Support |
Notes |
|---|---|---|
Static Hamiltonians |
✅ Mature |
Standard NumPy arrays |
Time-dependent Hamiltonians |
✅ Mature |
Returns parametrized function |
Collapse operators (static) |
⚠️ Limited |
Returns arrays, no integration |
Collapse operators (time-dependent) |
❌ Not supported |
Would require custom solver |
Open-system solvers |
❌ Not included |
Dense-matrix only |
Automatic differentiation |
❌ Not supported |
Use JAX instead |
Custom backends |
✅ Easy |
Reference implementation |
Debugging |
✅ Good |
Simple, easy to inspect arrays |
- Use NumPy when:
Prototyping new models quickly
You only need dense Hamiltonian matrices
Learning the library (simplest backend)
Educational use
Known Limitations#
Before you start, be aware of these constraints and gotchas:
DSL Constraints
Operator functions cannot contain operator sums inside
✅ Allowed:
\cos(\sigma_z + \alpha) % Scalar sum + single operator OK \sin(\sigma_z) % Single operator OK
❌ Rejected:
\cos(\sigma_x + \sigma_y) % Operator sum inside function \exp(\sigma_z \sigma_x) % Multiple operators
Workaround: Expand manually or use the IR directly.
Time-dependent collapse operators must be single monomials
✅ Allowed:
\sqrt{\gamma} \exp(-t/2) \sigma_- % Single operator term
❌ Rejected:
\sqrt{\gamma_1} \sigma_{-,1} + \sqrt{\gamma_2} \sigma_{-,2} % Sum
Workaround: Use two separate collapse operator strings.
Symbolic expansions are capped at 512 terms
Large powers like \((a + b)^{100}\) will raise an error to prevent memory blow-up.
Workaround: Expand by hand or rewrite the Hamiltonian.
Backend Limitations
QuTiP is dense-matrix only — Large systems (>15 qubits) become slow
JAX: No built-in dissipation solvers — Open systems require custom integration
NumPy: No optimization — Slowest backend for production use
Time-dependent collapse operators: QuTiP-only — JAX and NumPy don’t support
Parameter Handling
Ambiguous parameter aliases — If you have both
omega_candomega_c1, the first match winsparams = {"omega_c": 1.0, "omega_c1": 2.0} # Which does \omega_c match? Answer: the first in the params dict (ambiguous!)
Solution: Use unambiguous names or inspect
ir.free_symbolsto debugParameter validation is strict — Missing or misnamed parameters raise early
H = r"\omega \sigma_z" params = {"omega": 1.0} # But your LaTeX uses \omega_0, not \omega? # → DSLValidationError: "Missing numeric value for omega_0"
Solution: Check parameter names carefully and use aliases
Quantum Numbers
Boson cutoffs are **not automatically validated**
If your system has fast oscillations, a cutoff=5 may be too small.
Solution: Check convergence by increasing cutoff and comparing results
No automatic dimension checking
You can accidentally create an over-truncated system. The library won’t warn you.
Compilation Speed
Large systems are slow — Many qubits + complex time-dependence can take seconds
Example: 10 qubits + 5 time-dependent terms ≈ 2–5 seconds to compile
Solution: Use static systems while developing, add time-dependence once validated
Symbolic simplification is deferred — Complex expressions aren’t simplified before backend dispatch
Backend Differences
- The same LaTeX may produce different numerical results with different backends due to:
Floating-point precision differences
Different truncation strategies (for sparse representations, if used)
JAX JIT compilation precision
Solution: Test with QuTiP first, then switch backends
Type System
- The library uses Python type hints. IDEs should help, but:
Custom backends must implement the
BackendBaseinterfaceErrors from incorrect types are caught at runtime, not compile-time
—
Next: Browse Examples for more workflows, or jump to API Reference for detailed reference.