Source code for quchip.declarative.expr

"""One backend-neutral expression tree for authored scalar and operator physics."""

from __future__ import annotations

import inspect
from collections.abc import Iterable, Iterator
from dataclasses import dataclass, field, fields, is_dataclass, replace
from math import prod
from numbers import Number
from typing import Any, Mapping

import jax.numpy as jnp
import numpy as np

from quchip.utils.jax_utils import concrete_array_module
from quchip.utils.values import copy_value, value_fingerprint


[docs] class UnboundParameterError(ValueError): """Numerical materialization started without all required values."""
[docs] def is_opaque_callable(value: Any) -> bool: """Return whether *value* is a callable authoring function, not a matrix-like object.""" return callable(value) and getattr(value, "shape", None) is None
[docs] @dataclass(frozen=True) class PhysicsExpr: """Authored scalar and operator algebra, independent of numerical values. Parameters ---------- kind : str Expression node kind, such as ``"op"``, ``"add"``, ``"parameter"`` or ``"matrix"``. args : tuple, default () Node payload in the representation that ``kind`` requires. labels : tuple[str, ...], default () Ordered endpoint labels for this expression's support. """ kind: str args: tuple[Any, ...] = () labels: tuple[str, ...] = () _bindings: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
[docs] @classmethod def parameter( cls, *, scope: str, name: str, symbol: str | None = None, unit: str | None = None, ) -> "PhysicsExpr": """Create a symbolic declared-parameter leaf. Parameters ---------- scope, name : str Dotted owner scope and declared field name. symbol, unit : str or None Optional display symbol and unit metadata. """ return cls("parameter", (f"{scope}.{name}", symbol or name, unit))
[docs] @classmethod def literal(cls, value: Any) -> "PhysicsExpr": """Create a literal scalar leaf. Parameters ---------- value : scalar Numerical constant copied into the expression. """ return cls("literal", (copy_value(value, readonly=True),))
[docs] @classmethod def from_matrix( cls, value: Any, *, labels: tuple[str, ...], dims: tuple[int, ...], name: str | None = None, excitation_changes: Iterable[int] | None = None, ) -> "PhysicsExpr": """Create a named backend-neutral matrix contribution. Parameters ---------- value : array-like Matrix payload with one local block per ``dims`` entry. labels, dims : tuple Matching ordered endpoint labels and positive dimensions. name : str or None Optional display name. excitation_changes : iterable of int or None Total energy-level changes that the matrix can carry (column minus row in the captured energy bases). Band decomposition treats all other total changes as structural zeros, even for traced payloads. ``None`` declares no structure. """ if len(labels) != len(dims): raise ValueError("Matrix labels and dimensions must have the same length.") changes = None if excitation_changes is None else frozenset(int(c) for c in excitation_changes) return cls("matrix", (copy_value(value, readonly=True), tuple(dims), name, changes), tuple(labels))
[docs] @classmethod def from_function( cls, function: Any, *arguments: Any, labels: tuple[str, ...], dims: tuple[int, ...], name: str | None = None, ) -> "PhysicsExpr": """Create an opaque matrix-valued contribution from a pure function. The function runs only during numerical materialization. Display keeps its declared name and arguments, such as ``X(a, b)``, and does not show the implementation as symbolic algebra. Parameters ---------- function : callable Pure callable evaluated during numerical materialization. *arguments : Any Expression arguments passed to ``function``. labels, dims : tuple Matching endpoint labels and dimensions. name : str or None Display name, required for anonymous callables. """ if len(labels) != len(dims): raise ValueError("Function labels and dimensions must have the same length.") if not callable(function): raise TypeError("function must be callable.") display_name = name or getattr(function, "__name__", None) if not display_name or display_name == "<lambda>": raise ValueError("Anonymous functions require a symbolic name.") return cls( "function", (function, tuple(dims), display_name, *(ensure_expr(arg) for arg in arguments)), tuple(labels), )
[docs] @classmethod def from_state( cls, value: Any, *, labels: tuple[str, ...], dims: tuple[int, ...], name: str, ) -> "PhysicsExpr": """Create a named authored ket contribution. Parameters ---------- value : array-like Ket payload. labels, dims : tuple Matching endpoint labels and dimensions. name : str Display name. """ return cls("state", (copy_value(value, readonly=True), tuple(dims), name), tuple(labels))
[docs] @classmethod def from_state_function( cls, function: Any, *arguments: Any, labels: tuple[str, ...], dims: tuple[int, ...], name: str, ) -> "PhysicsExpr": """Create an opaque callable ket contribution. Parameters ---------- function : callable Callable that returns a ket during materialization. *arguments : Any Expression arguments passed to ``function``. labels, dims : tuple Matching endpoint labels and dimensions. name : str Display name. """ if not callable(function): raise TypeError("function must be callable.") return cls( "state_function", (function, tuple(dims), name, *(ensure_expr(arg) for arg in arguments)), tuple(labels), )
[docs] @classmethod def from_signal(cls, signal: Any, *, name: str = "f") -> "PhysicsExpr": """Create a scalar time-function leaf backed by an engine signal. Parameters ---------- signal : object Engine signal object evaluated at runtime. name : str, default ``"f"`` Display name for the signal node. """ return cls("signal", (signal, name))
[docs] def embed(self, labels: tuple[str, ...], dims: tuple[int, ...]) -> "PhysicsExpr": """Embed this local contribution into an ordered composite Hilbert space. Parameters ---------- labels, dims : tuple Complete ordered composite labels and matching dimensions. """ if len(labels) != len(dims): raise ValueError("Composite labels and dimensions must have the same length.") missing = set(self.labels) - set(labels) if missing: raise ValueError(f"Cannot embed labels absent from the composite space: {sorted(missing)}") return PhysicsExpr("embed", (self, tuple(labels), tuple(dims)), tuple(labels))
[docs] def with_bindings(self, bindings: Mapping[str, Any]) -> "PhysicsExpr": """Attach default values that only direct numerical inspection uses. Parameters ---------- bindings : mapping[str, Any] Dotted parameter paths to numerical values. """ return replace(self, _bindings=copy_value(dict(bindings), readonly=True))
[docs] def parameter_paths(self) -> tuple[str, ...]: """Return referenced dotted parameter paths in authored order.""" return tuple(dict.fromkeys( node.args[0] for node in _walk_expr(self) if node.kind == "parameter" ))
[docs] def numeric_values(self) -> tuple[Any, ...]: """Return bound and matrix payloads for tracer-safe cache decisions.""" values: list[Any] = [] for node in _walk_expr(self): values.extend(node._bindings.values()) if node.kind in ("literal", "matrix", "state", "signal"): values.append(node.args[0]) if node.kind == "level" and not isinstance(node.args[0], PhysicsExpr): values.append(node.args[0]) return tuple(values)
@property def shape(self) -> tuple[int, int]: """Matrix shape implied by this operator expression's static support.""" if not self.labels: raise AttributeError("Scalar expressions do not have a matrix shape.") if self.kind == "level": return self.args[0].shape if self.kind == "op": dimension = self.args[1].dimension return (dimension, dimension) if self.kind in ("matrix", "function"): dimension = prod(self.args[1]) return (dimension, dimension) if self.kind in ("state", "state_function"): return (prod(self.args[1]), 1) if self.kind == "embed": dimension = prod(self.args[2]) return (dimension, dimension) if self.kind == "tensor": dimension = self.args[0].shape[0] * self.args[1].shape[0] return (dimension, dimension) for arg in self.args: if isinstance(arg, PhysicsExpr) and arg.labels: return arg.shape raise AttributeError("Operator shape is not available for this expression.") def _binary(self, other: Any, kind: str) -> "PhysicsExpr": rhs = ensure_expr(other) if kind in ("add", "sub"): if bool(self.labels) != bool(rhs.labels): raise TypeError( "Cannot add a scalar directly to an operator; write the identity explicitly." ) if self.labels != rhs.labels: raise TypeError( "Addition and subtraction require operands with the same endpoint support " f"(got {self.labels!r} and {rhs.labels!r})." ) return PhysicsExpr( kind, (self, rhs), tuple(dict.fromkeys(self.labels + rhs.labels)), ) def __add__(self, other: Any) -> "PhysicsExpr": return self._binary(other, "add") def __radd__(self, other: Any) -> "PhysicsExpr": return ensure_expr(other)._binary(self, "add") def __sub__(self, other: Any) -> "PhysicsExpr": return self._binary(other, "sub") def __rsub__(self, other: Any) -> "PhysicsExpr": return ensure_expr(other)._binary(self, "sub") def __matmul__(self, other: Any) -> "PhysicsExpr": rhs = ensure_expr(other) if not self.labels or self.labels != rhs.labels: raise TypeError("Cannot use @ for operators on different endpoints; use * across endpoints.") return self._binary(rhs, "matmul") def __mul__(self, other: Any) -> "PhysicsExpr": rhs = ensure_expr(other) if self.labels and rhs.labels: if set(self.labels) & set(rhs.labels): raise TypeError( "Cannot use * for operators on the same endpoint or with overlapping " "endpoint support; use @ on one endpoint." ) return PhysicsExpr( "tensor", (self, rhs), tuple(dict.fromkeys(self.labels + rhs.labels)), ) if self.labels or rhs.labels: scalar, operator = (rhs, self) if self.labels else (self, rhs) return PhysicsExpr("scale", (scalar, operator), operator.labels) return PhysicsExpr("mul", (self, rhs)) def __rmul__(self, other: Any) -> "PhysicsExpr": return ensure_expr(other).__mul__(self) def __truediv__(self, other: Any) -> "PhysicsExpr": rhs = ensure_expr(other) if rhs.labels: raise TypeError("Division by an operator is not defined.") return self * PhysicsExpr("pow", (rhs, PhysicsExpr.literal(-1))) def __rtruediv__(self, other: Any) -> "PhysicsExpr": if self.labels: raise TypeError("Division by an operator is not defined.") return ensure_expr(other) * PhysicsExpr( "pow", (self, PhysicsExpr.literal(-1)) ) def __pow__(self, other: Any) -> "PhysicsExpr": rhs = ensure_expr(other) if self.labels or rhs.labels: raise TypeError("Use @ for operator powers; ** is scalar-only.") return PhysicsExpr("pow", (self, rhs)) def __neg__(self) -> "PhysicsExpr": return -1 * self
[docs] def latex(self) -> str: """Render the authored expression with familiar mathematical notation.""" return _latex(self)
def _repr_latex_(self) -> str: return f"${self.latex()}$" def __str__(self) -> str: return self.latex()
[docs] def matrix( self, bindings: Mapping[str, Any] | None = None, *, t: Any | None = None, backend: Any = None, ) -> Any: """Materialize this expression and return its dense numerical array. Parameters ---------- bindings : mapping or None, optional Values for unresolved parameter paths. t : scalar or None, keyword-only Time for signal evaluation. backend : backend or None, keyword-only Backend lowering object. ``None`` selects the default. """ if backend is None: from quchip.backend import get_default_backend backend = get_default_backend() native = materialize_expr( self, backend, bindings=bindings, t=t, ) return backend.to_array(native)
[docs] class ParameterNamespace: """Attribute view that exposes one owner's declared fields as symbolic leaves.""" __slots__ = ("_scope", "_fields") def __init__(self, scope: str, fields: Mapping[str, Any]) -> None: self._scope = scope self._fields = fields def __getattr__(self, name: str) -> PhysicsExpr: if name == "thermal_population" and "thermal_occupation" in self._fields: from quchip.utils.deprecation import warn_renamed warn_renamed(name, "thermal_occupation") name = "thermal_occupation" try: spec = self._fields[name] except KeyError as exc: raise AttributeError(f"No declared parameter {name!r} on {self._scope!r}.") from exc return PhysicsExpr.parameter( scope=self._scope, name=name, symbol=spec.symbol, unit=spec.unit, ) def __dir__(self) -> list[str]: return sorted(self._fields)
[docs] def ensure_expr(value: Any) -> PhysicsExpr: """Coerce a scalar value into the shared expression tree.""" if isinstance(value, PhysicsExpr): return value if isinstance(value, (int, float, complex)) or getattr(value, "ndim", None) == 0: return PhysicsExpr.literal(value) raise TypeError(f"Expected a scalar or PhysicsExpr, got {type(value).__name__}.")
[docs] def as_operator_expr( value: Any, *, labels: tuple[str, ...], dims: tuple[int, ...], name: str, arguments: tuple[Any, ...] = (), owner: Any | None = None, scope: str | None = None, allowed: Mapping[str, Any] | None = None, ) -> PhysicsExpr: """Normalize symbolic, matrix, or opaque callable operator authorship. Parameters ---------- value : PhysicsExpr, callable, or matrix Authored operator representation. labels, dims : tuple Ordered endpoint labels and matching dimensions. name : str Display name for opaque or matrix contributions. arguments : tuple, default () Arguments for an opaque callable. owner : object or None, default None Object whose fields bind the positional argument names of a callable. Do not combine with explicit ``arguments``. scope : str or None, default None Dotted-path prefix for owner-bound parameters, such as a device label. Required with ``owner`` when the callable has arguments. allowed : mapping or None, default None Permitted owner-field names, given as mapping keys whose values are not used. ``None`` accepts all named owner fields. """ expected = (prod(dims), prod(dims)) if isinstance(value, PhysicsExpr): if not value.labels: raise TypeError("An operator expression must carry at least one subsystem label.") if value.labels != labels: raise TypeError( f"Operator expression support {value.labels!r} does not match {labels!r}." ) if value.shape != expected: raise ValueError( f"Operator expression has shape {value.shape}, expected {expected}." ) return value if is_opaque_callable(value): arguments, bindings = _resolve_callable_arguments( value, arguments=arguments, owner=owner, scope=scope, allowed=allowed ) return PhysicsExpr.from_function( value, *arguments, labels=labels, dims=dims, name=name, ).with_bindings(bindings) if getattr(value, "shape", None) != expected: raise TypeError( f"Operator must be a PhysicsExpr, callable, or matrix with shape {expected}; " f"got {type(value).__name__} with shape {getattr(value, 'shape', None)}." ) return PhysicsExpr.from_matrix(value, labels=labels, dims=dims, name=name)
[docs] def as_scalar_expr( value: Any, *, name: str, arguments: tuple[Any, ...] = (), owner: Any | None = None, scope: str | None = None, allowed: Mapping[str, Any] | None = None, ) -> PhysicsExpr: """Normalize symbolic, numeric, or opaque callable scalar authorship. Parameters ---------- value : PhysicsExpr, scalar, or callable Authored scalar representation. name : str Display name for opaque callables. arguments : tuple, default () Arguments for an opaque callable. owner : object or None, default None Object whose fields bind the positional argument names of a callable. Do not combine with explicit ``arguments``. scope : str or None, default None Dotted-path prefix for owner-bound parameters, such as a device label. Required with ``owner`` when the callable has arguments. allowed : mapping or None, default None Permitted owner-field names, given as mapping keys whose values are not used. ``None`` accepts all named owner fields. """ if isinstance(value, PhysicsExpr): if value.labels: raise TypeError("A scalar expression cannot carry subsystem labels.") return value if is_opaque_callable(value): arguments, bindings = _resolve_callable_arguments( value, arguments=arguments, owner=owner, scope=scope, allowed=allowed ) return PhysicsExpr.from_function( value, *arguments, labels=(), dims=(), name=name, ).with_bindings(bindings) return ensure_expr(value)
[docs] def as_state_expr( value: Any, *, labels: tuple[str, ...], dims: tuple[int, ...], name: str, arguments: tuple[Any, ...] = (), owner: Any | None = None, scope: str | None = None, allowed: Mapping[str, Any] | None = None, ) -> PhysicsExpr: """Normalize an authored ket array or opaque callable without evaluating it. Parameters ---------- value : PhysicsExpr, callable, or array Authored ket representation. labels, dims : tuple Ordered endpoint labels and matching dimensions. name : str Display name for the state contribution. arguments : tuple, default () Arguments for an opaque callable. owner : object or None, default None Object whose fields bind the positional argument names of a callable. Do not combine with explicit ``arguments``. scope : str or None, default None Dotted-path prefix for owner-bound parameters, such as a device label. Required with ``owner`` when the callable has arguments. allowed : mapping or None, default None Permitted owner-field names, given as mapping keys whose values are not used. ``None`` accepts all named owner fields. """ expected = (prod(dims), 1) if isinstance(value, PhysicsExpr): if value.labels != labels: raise ValueError( f"State expression support {value.labels!r} does not match {labels!r}." ) if value.shape != expected: raise ValueError( f"State expression has shape {value.shape}, expected {expected}." ) return value if is_opaque_callable(value): arguments, bindings = _resolve_callable_arguments( value, arguments=arguments, owner=owner, scope=scope, allowed=allowed ) return PhysicsExpr.from_state_function( value, *arguments, labels=labels, dims=dims, name=name, ).with_bindings(bindings) shape = getattr(value, "shape", None) if shape not in (expected, (expected[0],)): raise TypeError( f"State must be a PhysicsExpr, callable, or ket with shape {expected} or {(expected[0],)}; " f"got {type(value).__name__} with shape {shape}." ) return PhysicsExpr.from_state(value, labels=labels, dims=dims, name=name)
def _resolve_callable_arguments( function: Any, *, arguments: tuple[Any, ...], owner: Any | None, scope: str | None, allowed: Mapping[str, Any] | None = None, ) -> tuple[tuple[PhysicsExpr, ...], dict[str, Any]]: """Resolve explicit callable arguments or bind their names to an owner.""" if not is_opaque_callable(function): return (), {} if arguments: if owner is not None: raise TypeError("Pass explicit callable arguments or an owner, not both.") return tuple(ensure_expr(argument) for argument in arguments), {} if owner is None or scope is None: if inspect.signature(function).parameters: raise TypeError("Opaque functions with parameters require an owner and scope.") return (), {} fields = getattr(type(owner), "__quchip_param_fields__", {}) resolved_arguments: list[PhysicsExpr] = [] bindings: dict[str, Any] = {} for item in inspect.signature(function).parameters.values(): if item.kind not in (item.POSITIONAL_ONLY, item.POSITIONAL_OR_KEYWORD): raise TypeError("Opaque functions may only declare positional parameters.") if allowed is not None and item.name not in allowed: raise ValueError( f"Opaque function argument {item.name!r} is not declared; " f"available fields are {sorted(allowed)}." ) if not hasattr(owner, item.name): raise ValueError(f"Opaque function argument {item.name!r} is not a field on {scope!r}.") spec = fields.get(item.name) path = f"{scope}.{item.name}" resolved_arguments.append( PhysicsExpr.parameter( scope=scope, name=item.name, symbol=getattr(spec, "symbol", None) or item.name, unit=getattr(spec, "unit", None), ) ) bindings[path] = getattr(owner, item.name) return tuple(resolved_arguments), bindings def _walk_expr(expr: PhysicsExpr) -> Iterator[PhysicsExpr]: """Yield an expression tree in authored preorder.""" yield expr for arg in expr.args: if isinstance(arg, PhysicsExpr): yield from _walk_expr(arg)
[docs] def split_dynamic_hamiltonian(expr: PhysicsExpr) -> tuple[tuple[PhysicsExpr, PhysicsExpr], ...]: """Split a linear Hamiltonian sum into scalar-signal and operator factors. Signal algebra can be nonlinear and can contain more than one signal leaf. Only the quantum operator must be linear. Each additive term must be one operator-valued factor times a scalar expression that depends on at least one delivered signal. """ if not isinstance(expr, PhysicsExpr) or not expr.labels: raise TypeError("A drive Hamiltonian must return an operator-valued PhysicsExpr.") def additive_terms(node: PhysicsExpr) -> list[tuple[int, PhysicsExpr]]: if node.kind == "add": return additive_terms(node.args[0]) + additive_terms(node.args[1]) if node.kind == "sub": return additive_terms(node.args[0]) + [ (-sign, term) for sign, term in additive_terms(node.args[1]) ] return [(1, node)] terms: list[tuple[PhysicsExpr, PhysicsExpr]] = [] for sign, term in additive_terms(expr): if term.kind != "scale": raise TypeError( "Each drive Hamiltonian term must multiply a delivered signal by a quantum operator." ) scalar, operator = term.args if scalar.labels or not operator.labels: raise TypeError("Drive Hamiltonian terms must be scalar-signal times operator.") if not any(node.kind == "signal" for node in _walk_expr(scalar)): raise TypeError("Drive Hamiltonian scalar factors must depend on the delivered signal.") terms.append((scalar if sign > 0 else -scalar, operator)) return tuple(terms)
[docs] def scalar_signal_program(expr: PhysicsExpr) -> Any: """Lower scalar signal algebra into the backend-neutral signal program.""" from quchip.engine.ir import Add, Constant, Multiply, Scale, SignalPower values = _bound_values(expr) def lower(node: PhysicsExpr) -> Any: if node.labels: raise TypeError("Signal-program lowering accepts scalar expressions only.") if node.kind == "signal": return node.args[0] if node.kind == "literal": return Constant(node.args[0]) if node.kind == "parameter": try: return Constant(values[node.args[0]]) except KeyError as exc: raise UnboundParameterError( f"Missing numerical binding: {node.args[0]}" ) from exc if node.kind == "add": return Add((lower(node.args[0]), lower(node.args[1]))) if node.kind == "sub": return Add((lower(node.args[0]), Scale(lower(node.args[1]), -1.0))) if node.kind == "mul": return Multiply((lower(node.args[0]), lower(node.args[1]))) if node.kind == "pow": exponent = node.args[1] if exponent.kind != "literal": raise TypeError("Signal powers require a literal exponent.") return SignalPower(lower(node.args[0]), exponent.args[0]) raise TypeError(f"Unsupported scalar signal expression kind {node.kind!r}.") return lower(expr)
def _bound_values(expr: PhysicsExpr, bindings: Mapping[str, Any] | None = None) -> dict[str, Any]: values: dict[str, Any] = {} for node in _walk_expr(expr): values.update(node._bindings) if bindings is not None: values.update(bindings) return values def _same_authored_value(first: Any, second: Any) -> bool: """Compare known authored structure and values, using identity for tracers.""" from quchip.utils.jax_utils import contains_tracer if first is second: return True if type(first) is not type(second): return False if is_dataclass(first) and not isinstance(first, type): return all(_same_authored_value(getattr(first, item.name), getattr(second, item.name)) for item in fields(first) if not (isinstance(first, PhysicsExpr) and item.name == "_bindings")) if isinstance(first, (tuple, list)): return len(first) == len(second) and all(_same_authored_value(a, b) for a, b in zip(first, second)) if contains_tracer((first, second)): return False try: return value_fingerprint(first) == value_fingerprint(second) except (ValueError, RecursionError): return False def _matching_local_basis( level: PhysicsExpr, bases: Mapping[str, Any] | None, bindings: Mapping[str, Any], ) -> Any | None: record = None if bases is None else bases.get(level.labels[0]) if record is None or not _same_authored_value(level.args[0], record.authored_hamiltonian): return None hamiltonian = record.authored_hamiltonian if isinstance(hamiltonian, PhysicsExpr): expected = _bound_values(hamiltonian) if not all(path in bindings and path in expected and _same_authored_value(bindings[path], expected[path]) for path in hamiltonian.parameter_paths()): return None return record
[docs] def declared_excitation_changes(expr: Any) -> frozenset[int] | None: """Return the total energy-level changes a matrix contribution declares, if any.""" if isinstance(expr, PhysicsExpr) and expr.kind == "matrix": return expr.args[3] return None
def _authored_entry_support(expr: Any) -> np.ndarray | None: """Return a bound on the nonzero entries of authored operator algebra, or ``None``. Scalar coefficients do not change the bound, so it also holds for traced values. """ import jax from quchip.utils.jax_utils import contains_tracer if isinstance(expr, PhysicsExpr): if expr.kind == "scale": return _authored_entry_support(expr.args[1]) if expr.kind in ("add", "sub", "matmul", "tensor"): left, right = (_authored_entry_support(arg) for arg in expr.args) if left is None or right is None: return None if expr.kind in ("add", "sub"): return left | right return left @ right if expr.kind == "matmul" else np.kron(left, right) if expr.kind not in ("op", "matrix") or contains_tracer(expr.numeric_values()): return None with jax.ensure_compile_time_eval(): expr = materialize_array(expr) if contains_tracer(expr): return None values = np.asarray(expr) return values != 0 if values.ndim == 2 else None
[docs] def is_energy_diagonal(expr: Any, bases: Mapping[str, Any]) -> bool: """Return whether authored algebra guarantees zero energy-change weight in captured bases.""" from quchip.devices.spaces import ChargeSpace, FockSpace, PhaseGridSpace if not isinstance(expr, PhysicsExpr): return False bindings = _bound_values(expr) def diagonal(node: PhysicsExpr) -> bool: if not node.labels: return True if node.kind == "level": return _matching_local_basis(node, bases, bindings) is not None if node.kind == "op": return node.args[0] == "I" and type(node.args[1]) in (FockSpace, ChargeSpace, PhaseGridSpace) if node.kind in ("add", "sub", "scale", "tensor", "matmul", "embed"): return all(diagonal(arg) for arg in node.args if isinstance(arg, PhysicsExpr)) return False return diagonal(expr)
[docs] def materialize_expr( expr: Any, backend: Any, *, bindings: Mapping[str, Any] | None = None, t: Any | None = None, local_bases: Mapping[str, Any] | None = None, ) -> Any: """Lower symbolic physics, and pass an already-native contribution through unchanged.""" if not isinstance(expr, PhysicsExpr): return expr values = _bound_values(expr, bindings) missing = [path for path in expr.parameter_paths() if path not in values] if missing: raise UnboundParameterError("Missing numerical bindings: " + ", ".join(missing)) combine = getattr(backend, "linear_combination", None) def lower(node: PhysicsExpr) -> Any: if node.kind == "literal": return node.args[0] if node.kind == "parameter": return values[node.args[0]] if node.kind == "signal": if t is None: raise ValueError("t is required to materialize a time-dependent expression.") from quchip.engine.ir import evaluate_signal_program return evaluate_signal_program(node.args[0], t, xp=backend.array_module) if node.kind == "matrix": value, dims, *_ = node.args return backend.from_array( value, dims=[list(dims), list(dims)], ) if node.kind == "state": value, dims, _name = node.args return backend.from_array( value, dims=[list(dims), [1]], ) if node.kind == "function": function, dims, _name, *arguments = node.args value = function(*(lower(argument) for argument in arguments)) if not dims: return value return backend.from_array(value, dims=[list(dims), list(dims)]) if node.kind == "state_function": function, dims, _name, *arguments = node.args value = function(*(lower(argument) for argument in arguments)) return backend.from_array(value, dims=[list(dims), [1]]) if node.kind == "op": name, space = node.args return space.operator(name, backend) if node.kind == "level": import jax from quchip.engine.basis import resolve_local_basis record = _matching_local_basis(node, local_bases, values) if record is None: # Concrete authored values give a concrete basis, also inside a JAX trace. with jax.ensure_compile_time_eval(): record = resolve_local_basis(materialize_array(node.args[0], bindings=values), authored_hamiltonian=node.args[0]) if record.kind == "native" and record.energy_to_solver() is None: return backend.number(record.native_dim) return backend.from_array(record.authored_level_operator(), dims=[[record.native_dim], [record.native_dim]]) if node.kind == "embed": local = lower(node.args[0]) labels, dims = node.args[1:] support = tuple(labels.index(label) for label in node.args[0].labels) if len(support) == 1: return backend.embed(local, support[0], dims) if len(support) == 2: return backend.embed_two_body(local, support[0], support[1], dims) if support == tuple(range(len(dims))): return local raise ValueError(f"Cannot embed a contribution with support {support}.") left = lower(node.args[0]) right = lower(node.args[1]) # A backend that combines operators itself can form concrete sums # and scalings without device programs. if node.kind in ("add", "sub"): sign = 1 if node.kind == "add" else -1 if combine is not None and not (_is_scalar(left) or _is_scalar(right)): return combine(((1, left), (sign, right))) return left + right if sign == 1 else left - right if node.kind == "matmul": return backend.matmul(left, right) if node.kind == "tensor": return backend.tensor(left, right) if node.kind in ("scale", "mul"): if combine is not None and _is_scalar(left) != _is_scalar(right): scalar, operator = (left, right) if _is_scalar(left) else (right, left) return combine(((scalar, operator),)) return left * right if node.kind == "pow": exponent = node.args[1] if exponent.kind == "literal" and exponent.args[0] == -1: return 1.0 / left return left ** right raise TypeError(f"Unknown PhysicsExpr kind {node.kind!r}.") return lower(expr)
def _is_scalar(value: Any) -> bool: """Whether a lowered value is a number rather than an operator.""" return isinstance(value, Number) or getattr(value, "shape", None) == () class _ArrayLowerer: """Minimal operator algebra for backend-independent JAX materialization. Concrete operands combine on the host, so a constant operator compiles no XLA programs; traced operands stay in JAX. """ array_module = jnp @staticmethod def from_array(value: Any, dims: Any = None) -> Any: del dims if hasattr(value, "to_jax"): value = value.to_jax() elif hasattr(value, "full"): value = value.full() return concrete_array_module(value).asarray(value, dtype=complex) @staticmethod def to_array(value: Any) -> Any: if hasattr(value, "to_jax"): value = value.to_jax() elif hasattr(value, "full"): value = value.full() return jnp.asarray(value) @staticmethod def destroy(dimension: int) -> Any: return np.diag(np.sqrt(np.arange(1, dimension)), 1).astype(complex) @staticmethod def create(dimension: int) -> Any: return np.diag(np.sqrt(np.arange(1, dimension)), -1).astype(complex) @staticmethod def number(dimension: int) -> Any: return np.diag(np.arange(dimension, dtype=complex)) @staticmethod def identity(dimension: int) -> Any: return np.eye(dimension, dtype=complex) @staticmethod def dag(value: Any) -> Any: return concrete_array_module(value).asarray(value).conj().T @staticmethod def matmul(left: Any, right: Any) -> Any: return left @ right @staticmethod def tensor(left: Any, right: Any) -> Any: return concrete_array_module(left, right).kron(left, right) @staticmethod def embed(local: Any, target: int, dims: tuple[int, ...]) -> Any: xp = concrete_array_module(local) factors = [xp.eye(dim, dtype=complex) for dim in dims] factors[target] = local result = factors[0] for factor in factors[1:]: result = xp.kron(result, factor) return result @staticmethod def embed_two_body(local: Any, first: int, second: int, dims: tuple[int, ...]) -> Any: from quchip.backend._dims import _embed_array return _embed_array(local, (first, second), dims, concrete_array_module(local)) _ARRAY_LOWERER = _ArrayLowerer()
[docs] def materialize_array( expr: Any, *, bindings: Mapping[str, Any] | None = None, t: Any | None = None, ) -> Any: """Materialize authored physics as a backend-independent JAX array.""" return _ARRAY_LOWERER.to_array( materialize_expr(expr, _ARRAY_LOWERER, bindings=bindings, t=t) )
[docs] def materialize_scalar( expr: Any, *, bindings: Mapping[str, Any] | None = None, ) -> Any: """Materialize a scalar expression without coercing traced values.""" return materialize_expr(expr, _ARRAY_LOWERER, bindings=bindings)
def _latex(expr: PhysicsExpr, parent_precedence: int = 0) -> str: if expr.kind == "level": return rf"\hat \ell_{{{expr.labels[0]}}}" if expr.kind == "literal": value = expr.args[0] if isinstance(value, complex): return _complex_latex(value) return f"{value:g}" if isinstance(value, (int, float)) else str(value) if expr.kind == "parameter": path, symbol, _unit = expr.args scope = path.rsplit(".", 1)[0] return _scoped_symbol(symbol, scope) if expr.kind == "signal": _signal, name = expr.args return rf"{name}\!\left(t\right)" if expr.kind == "matrix": name = expr.args[2] if name is not None: return name return rf"\hat H_{{{','.join(expr.labels)}}}" if expr.kind == "state": _value, _dims, name = expr.args return name if expr.kind == "function": _function, _dims, name, *arguments = expr.args rendered = ", ".join(_latex(argument) for argument in arguments) return rf"{name}\!\left({rendered}\right)" if expr.kind == "state_function": _function, _dims, name, *arguments = expr.args rendered = ", ".join(_latex(argument) for argument in arguments) return rf"{name}\!\left({rendered}\right)" if expr.kind == "op": name, _space = expr.args scope = expr.labels[0] symbols = { "a": r"\hat a", "adag": r"\hat a^\dagger", "n": r"\hat n", "I": r"\hat I", "sigma_x": r"\hat\sigma_x", "sigma_y": r"\hat\sigma_y", "sigma_z": r"\hat\sigma_z", "sigma_plus": r"\hat\sigma_+", "sigma_minus": r"\hat\sigma_-", } symbol = symbols.get(name, rf"\hat{{\mathrm{{{name}}}}}") return _scoped_symbol(symbol, scope) if expr.kind == "embed": return _latex(expr.args[0], parent_precedence) if expr.kind in ("add", "sub"): precedence = 1 elif expr.kind in ("scale", "mul", "tensor"): precedence = 2 else: precedence = 3 left = _latex(expr.args[0], precedence) right = _latex(expr.args[1], precedence + (1 if expr.kind == "sub" else 0)) if expr.kind == "add": text = f"{left} + {right}" elif expr.kind == "sub": text = f"{left} - {right}" elif expr.kind in ("matmul", "tensor", "mul", "scale"): text = rf"{left}\,{right}" elif expr.kind == "pow": text = f"{left}^{{{right}}}" else: raise TypeError(f"Unknown PhysicsExpr kind {expr.kind!r}.") return f"({text})" if precedence < parent_precedence else text def _complex_latex(value: complex) -> str: """Render a complex literal with ``i`` as the imaginary unit.""" if value.imag == 0: return f"{value.real:g}" imaginary = {1.0: "i", -1.0: "-i"}.get(value.imag, f"{value.imag:g}i") if value.real == 0: return imaginary sign = "" if imaginary.startswith("-") else "+" return f"({value.real:g}{sign}{imaginary})" def _scoped_symbol(symbol: str, scope: str) -> str: """Attach an owner scope without producing nested LaTeX subscripts.""" if "_" not in symbol: return f"{symbol}_{{{scope}}}" base, subscript = symbol.split("_", 1) subscript = subscript.removeprefix("{").removesuffix("}") return f"{base}_{{{subscript},{scope}}}"