IR Debugging and Inspection#
This example shows how to inspect the Intermediate Representation (IR) produced by the parser. It is useful when debugging parsing issues, parameter resolution, or operator recognition.
What you’ll learn#
How to call
latex_to_irand inspectir.termsandir.has_time_dep.How to use IR inspection to diagnose missing parameters and operator parsing.
Source#
1"""
2IR debugging and validation cookbook.
3
4This example is meant for developers who need to:
5- Inspect the intermediate representation (IR) produced from LaTeX.
6- Understand how time dependence is detected.
7- Diagnose missing-parameter errors before hitting a backend.
8- See how operator ordering is preserved in the IR.
9
10Run functions individually from a REPL; nothing executes on import.
11"""
12
13# flake8: noqa
14from __future__ import annotations
15
16import sys
17from pathlib import Path
18from pprint import pprint
19from typing import Iterable
20
21import sympy as sp
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_utils import collect_parameter_names, validate_required_params
28from latex_parser.dsl import BosonSpec, HilbertConfig, QubitSpec
29from latex_parser.ir import HamiltonianIR, latex_to_ir, parse_latex_expr
30
31
32def _print_terms(ir: HamiltonianIR) -> None:
33 """Helper: pretty-print IR terms."""
34 print("has_time_dep:", ir.has_time_dep)
35 for idx, term in enumerate(ir.terms):
36 print(f"Term {idx}: scalar={term.scalar_expr}")
37 print(" ops:", term.ops)
38
39
40def inspect_basic_ir() -> None:
41 """
42 Parse a simple driven qubit and print IR contents.
43 """
44 cfg = HilbertConfig(qubits=[QubitSpec(label="q", index=1)], bosons=[], customs=[])
45 H_latex = r"\frac{\omega_0}{2} \sigma_{z,1} + A \cos(\omega t) \sigma_{x,1}"
46 ir = latex_to_ir(H_latex, cfg, t_name="t")
47 _print_terms(ir)
48
49
50def inspect_operator_ordering() -> None:
51 """
52 Show that operator ordering is preserved in the IR.
53
54 Note: SymPy may reorder internally; the IR preserves the product order
55 after non-commutative handling in `latex_to_ir`.
56 """
57 cfg = HilbertConfig(
58 qubits=[],
59 bosons=[BosonSpec(label="a", index=1, cutoff=3)],
60 customs=[],
61 )
62 H_latex = r"a_{1}^{\dagger} a_{1}"
63 ir = latex_to_ir(H_latex, cfg, t_name="t")
64 _print_terms(ir)
65
66
67def detect_time_dependence() -> None:
68 """
69 Show how time dependence is flagged via scalar free symbols.
70 """
71 cfg = HilbertConfig(qubits=[QubitSpec(label="q", index=1)], bosons=[], customs=[])
72 H_td = r"A \cos(\omega t) \sigma_{x,1}"
73 H_static = r"\frac{\omega_0}{2} \sigma_{z,1}"
74 ir_td = latex_to_ir(H_td, cfg, t_name="t")
75 ir_static = latex_to_ir(H_static, cfg, t_name="t")
76 print("TD term:")
77 _print_terms(ir_td)
78 print("Static term:")
79 _print_terms(ir_static)
80
81
82def validate_params_against_ir() -> None:
83 """
84 Demonstrate param collection + validation without running a backend.
85 """
86 cfg = HilbertConfig(qubits=[QubitSpec(label="q", index=1)], bosons=[], customs=[])
87 H = r"\omega \sigma_{z,1} + g \cos(\nu t) \sigma_{x,1}"
88 c_ops = [r"\sqrt{\gamma} \sigma_{-,1}"]
89 ir_H = latex_to_ir(H, cfg, t_name="t")
90 time_names = {"t"}
91 required = collect_parameter_names(ir_H, cfg, time_names)
92 for c in c_ops:
93 ir_c = latex_to_ir(c, cfg, t_name="t")
94 required |= collect_parameter_names(ir_c, cfg, time_names)
95 print("Required symbols:", sorted(required))
96 params_ok = {"omega": 1.0, "g": 0.5, "nu": 2.0, "gamma": 0.1}
97 params_missing = {"omega": 1.0}
98 validate_required_params(required, params_ok, time_names)
99 try:
100 validate_required_params(required, params_missing, time_names)
101 except Exception as exc: # noqa: BLE001 - user-facing demo
102 print("Expected failure:", exc)
103
104
105def parse_latex_directly(exprs: Iterable[str]) -> None:
106 """
107 Show the raw SymPy expressions produced by `parse_latex_expr`.
108 """
109 for latex in exprs:
110 expr = parse_latex_expr(latex)
111 print("LaTeX:", latex)
112 print("SymPy:", expr)
113 print("Free symbols:", [s.name for s in expr.free_symbols])
114 print("-")
115
116
117def explore_symbol_aliases() -> None:
118 """
119 Display how different spellings map to the same SymPy symbol names.
120 """
121 cases = [
122 r"\omega_c",
123 r"\omega_{c}",
124 r"\omega_{ c }",
125 r"\omega_{c} t",
126 ]
127 parse_latex_directly(cases)
128
129
130def inspect_operator_functions() -> None:
131 """
132 Show operator-valued function handling in the IR.
133 """
134 cfg = HilbertConfig(qubits=[QubitSpec(label="q", index=1)], bosons=[], customs=[])
135 exprs = [
136 r"\exp(\sigma_{z,1})",
137 r"\cos(\phi) \sigma_{x,1}", # scalar cos(phi) times operator
138 r"\exp(\sigma_{z,1}) \sigma_{x,1}", # invalid operator-valued scalar
139 ]
140 valid = []
141 invalid = []
142 for e in exprs:
143 try:
144 ir = latex_to_ir(e, cfg, t_name="t")
145 valid.append((e, ir))
146 except Exception as exc: # noqa: BLE001 - user-facing demo
147 invalid.append((e, exc))
148 print("Valid expressions:")
149 for e, ir in valid:
150 print(" ", e)
151 _print_terms(ir)
152 print("Invalid expressions:")
153 for e, exc in invalid:
154 print(" ", e, "->", exc)
155
156
157def show_ir_math_ops() -> None:
158 """
159 Apply simple SymPy math to IR scalar parts (e.g., simplify).
160 """
161 cfg = HilbertConfig(qubits=[QubitSpec(label="q", index=1)], bosons=[], customs=[])
162 H = r"g (1+1) \sigma_{x,1}"
163 ir = latex_to_ir(H, cfg, t_name="t")
164 _print_terms(ir)
165 simplified = []
166 for term in ir.terms:
167 scalar_simplified = sp.simplify(term.scalar_expr)
168 simplified.append((scalar_simplified, term.ops))
169 print("After SymPy simplify:")
170 pprint(simplified)
171
172
173def explore_time_symbol_overrides() -> None:
174 """
175 Demonstrate adding extra time-like symbols beyond the default t_name.
176 """
177 cfg = HilbertConfig(qubits=[QubitSpec(label="q", index=1)], bosons=[], customs=[])
178 H = r"A \cos(\omega s) \sigma_{x,1}"
179 ir = latex_to_ir(H, cfg, t_name="t", time_symbols=("s",))
180 print("Extra time symbol 's' yields has_time_dep =", ir.has_time_dep)
181 _print_terms(ir)
182
183
184def show_expansion_guard() -> None:
185 """
186 Illustrate the expansion guard for large operator sums.
187 """
188 cfg = HilbertConfig(qubits=[QubitSpec(label="q", index=1)], bosons=[], customs=[])
189 H = r"( \sigma_{x,1} + \sigma_{y,1} )^3"
190 ir = latex_to_ir(H, cfg, t_name="t")
191 print("Expanded term count:", len(ir.terms))
192 _print_terms(ir)
193
194
195def rescue_merged_time_scalars() -> None:
196 """
197 Show how merged time scalars (omega_{dt}) are rescued into omega_{d} * t.
198 """
199 cfg = HilbertConfig(qubits=[QubitSpec(label="q", index=1)], bosons=[], customs=[])
200 H = r"\omega_{dt} \sigma_{x,1}"
201 ir = latex_to_ir(H, cfg, t_name="t")
202 _print_terms(ir)
203
204
205if __name__ == "__main__":
206 inspect_basic_ir()
207 inspect_operator_ordering()
208 detect_time_dependence()
209 validate_params_against_ir()
210 explore_symbol_aliases()
211 inspect_operator_functions()
212 show_ir_math_ops()
213 explore_time_symbol_overrides()
214 show_expansion_guard()
215 rescue_merged_time_scalars()
Run#
python examples/example_ir_debugging.py
Notes#
The IR is the authoritative representation of what the parser understood.
Use it to confirm that symbols you expected to be operators are not accidentally treated as scalars, and vice versa.