Source code for feynlag.parameters

"""Model parameters: external (experiment-fixed) vs internal (derived).

- :class:`ExternalParameter`: fixed by experiment (``v``, ``m_h``, ``g``…),
  carries a numeric value for benchmarks / UFO param cards.
- :class:`InternalParameter`: derived from other parameters via a SymPy
  expression (tadpole solutions, mixing angles, inverted quartics).  The
  expression may be assigned *later* by the pipeline.
- :class:`ParameterSet`: dependency DAG over internal parameters with
  topological ordering, full symbolic resolution and numeric evaluation —
  exactly what a UFO ``parameters.py`` needs.
"""

import sympy as sp

__all__ = ["Parameter", "ExternalParameter", "InternalParameter",
           "ParameterSet"]


[docs] class Parameter: """Wraps a SymPy Symbol with physics metadata. Args: name: symbol name. tex: LaTeX string (defaults to ``name``). real: SymPy assumption (default True — most Lagrangian parameters). positive: SymPy assumption (use for VEVs, masses; enables ``sqrt`` simplifications per CONVENTIONS.md). unit_dim: mass dimension, for dimensional checks. """
[docs] def __init__(self, name, tex=None, real=True, positive=False, unit_dim=0): self.name = name self.tex = tex if tex is not None else name assumptions = {"positive": True} if positive else {"real": True} if real else {} self.symbol = sp.Symbol(name, **assumptions) self.unit_dim = unit_dim
@property def s(self): """The underlying SymPy symbol (shorthand for expressions).""" return self.symbol def __repr__(self): return f"{type(self).__name__}({self.name!r})" def _repr_latex_(self): tex = self.tex if self.tex != self.name else sp.latex(self.symbol) return f"$\\displaystyle {tex}$" # Allow parameters to be used directly in SymPy arithmetic. def _sympy_(self): return self.symbol def __add__(self, other): return self.symbol + sp.sympify(other) __radd__ = __add__ def __sub__(self, other): return self.symbol - sp.sympify(other) def __rsub__(self, other): return sp.sympify(other) - self.symbol def __mul__(self, other): return self.symbol * sp.sympify(other) __rmul__ = __mul__ def __truediv__(self, other): return self.symbol / sp.sympify(other) def __rtruediv__(self, other): return sp.sympify(other) / self.symbol def __pow__(self, other): return self.symbol ** sp.sympify(other) def __neg__(self): return -self.symbol
[docs] class ExternalParameter(Parameter): """Parameter fixed by experiment; input of the model. ``value`` may be a float or a SymPy expression in *other external* parameters' numeric values (evaluated at benchmark time). """ nature = "external"
[docs] def __init__(self, name, value=None, tex=None, real=True, positive=False, unit_dim=0): super().__init__(name, tex=tex, real=real, positive=positive, unit_dim=unit_dim) self.value = value
[docs] class InternalParameter(Parameter): """Parameter derived from others via ``expr`` (assignable later). Examples: ``μ₁₁²`` from a tadpole condition, ``λ₁`` inverted from ``m_h`` and ``v``, a mixing angle from a ``tan 2θ`` condition. """ nature = "internal"
[docs] def __init__(self, name, expr=None, tex=None, real=True, positive=False, unit_dim=0): super().__init__(name, tex=tex, real=real, positive=positive, unit_dim=unit_dim) self.expr = sp.sympify(expr) if expr is not None else None
def define(self, expr): """Assign (or reassign) the defining expression.""" self.expr = sp.sympify(expr) return self
[docs] class ParameterSet: """Collection of parameters with dependency resolution. Internal parameters may depend on other internal parameters; the dependency graph must be acyclic. :meth:`dependency_order` returns internals in evaluation order (what UFO's ``parameters.py`` requires). """
[docs] def __init__(self, *params): self._by_name = {} self._by_symbol = {} self.add(*params)
def add(self, *params): for p in params: if p.name in self._by_name and self._by_name[p.name] is not p: raise ValueError(f"duplicate parameter name: {p.name}") self._by_name[p.name] = p self._by_symbol[p.symbol] = p return self def __getitem__(self, key): if isinstance(key, sp.Symbol): return self._by_symbol[key] return self._by_name[key] def __contains__(self, key): return key in self._by_name or key in self._by_symbol def __iter__(self): return iter(self._by_name.values()) def __len__(self): return len(self._by_name) def _repr_latex_(self): rows = [] for p in self: tex = p.tex if p.tex != p.name else sp.latex(p.symbol) value = p.value if isinstance(p, ExternalParameter) else p.expr value_tex = sp.latex(sp.sympify(value)) if value is not None else "-" rows.append(f"{p.name} & {tex} & {p.nature} & {value_tex} \\\\\n\\hline\n") table = (r"\begin{array}{|c|c|c|c|}" + "\n\\hline\n" + r"\textbf{name} & \textbf{symbol} & \textbf{nature} & " r"\textbf{value/expr} \\" + "\n\\hline\n" + "".join(rows) + r"\end{array}") return f"$\\displaystyle {table}$" @property def externals(self): return [p for p in self if isinstance(p, ExternalParameter)] @property def internals(self): return [p for p in self if isinstance(p, InternalParameter)] def dependency_order(self): """Topologically sort internal parameters by their dependencies. Returns the internals in an order where every parameter appears after all internal parameters occurring in its ``expr``. Raises: ValueError: on an undefined internal (``expr is None``), a dependency cycle, or a dependency on an unknown symbol. """ internals = self.internals internal_symbols = {p.symbol for p in internals} external_symbols = {p.symbol for p in self.externals} deps = {} for p in internals: if p.expr is None: raise ValueError(f"internal parameter {p.name} has no " f"defining expression") free = p.expr.free_symbols unknown = free - internal_symbols - external_symbols if unknown: raise ValueError( f"internal parameter {p.name} depends on symbols not in " f"the ParameterSet: {sorted(unknown, key=str)}") deps[p.symbol] = free & internal_symbols ordered, resolved = [], set() remaining = dict(deps) while remaining: ready = [s for s, d in remaining.items() if d <= resolved] if not ready: cycle = sorted(remaining, key=str) raise ValueError(f"dependency cycle among internal " f"parameters: {cycle}") for s in sorted(ready, key=str): ordered.append(self._by_symbol[s]) resolved.add(s) del remaining[s] return ordered def resolve(self): """Map every internal symbol to an expression in externals only.""" resolution = {} for p in self.dependency_order(): resolution[p.symbol] = p.expr.subs(resolution) return resolution def numeric(self): """Numeric value of every parameter (externals must have values). Returns: dict ``{symbol: float/complex}``. """ values = {} for p in self.externals: if p.value is None: raise ValueError(f"external parameter {p.name} has no value") values[p.symbol] = sp.sympify(p.value) # substitute progressively so external expressions of externals work for s in list(values): values[s] = values[s].subs(values) for p in self.dependency_order(): values[p.symbol] = p.expr.subs(values) return {s: complex(v) if v.is_complex and not v.is_real else float(v) for s, v in values.items()}