"""Vertex definitions and vertex instantiation.
A Vertex is a template for an interaction term in S_int.
A VertexInstance is a concrete instantiation with freshly assigned indices.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Sequence
from .expressions import Symbol
from .fields import Field, FieldOperator
from .indices import IndexContext
[docs]
@dataclass(frozen=True)
class Vertex:
"""A vertex (interaction term) in S_int.
Examples:
# Local vertex: int F_{ijk} phi_i(x) phi_j(x) psi_k(x) dx
Vertex(fields=[phi, phi, psi], coupling='F')
# Non-local vertex: iint K_{ij}(x,x') psi_i(x) psi_j(x') dx dx'
Vertex(fields=[psi, psi], coupling='K', local=False)
"""
fields: tuple[Field, ...]
coupling: str
local: bool = True
equal_time: bool = False # non-local only; collapse m time legs into one
# When True, every ψ leg of this non-local vertex has had its
# R-propagator pre-integrated into the coupling callable
# (``κ^(m)_R``). At DiagramTerm construction the R-propagators
# attached to this vertex's legs are tagged for absorption: each
# leg's time / direction is aliased onto its Wick partner's, and
# the integrand R-product loop skips those R-factors. Always
# ``False`` for local vertices (a local vertex's legs sit at one
# shared spacetime point, so there is nothing to absorb across).
already_R_contracted: bool = False
def __init__(
self,
fields: Sequence[Field],
coupling: str,
local: bool = True,
equal_time: bool = False,
already_R_contracted: bool = False,
):
if equal_time and local:
raise ValueError(
"equal_time=True is only valid for non-local vertices "
"(local=False). A local vertex already shares one "
"spatial / time leg across all fields, so there is "
"nothing to collapse."
)
if already_R_contracted and local:
raise ValueError(
"already_R_contracted=True is only valid for non-local "
"vertices (local=False). A local vertex's legs sit at a "
"single spacetime point, so R-absorption is vacuous."
)
if already_R_contracted and equal_time:
raise ValueError(
"already_R_contracted=True and equal_time=True are "
"mutually exclusive on a non-local vertex. The "
"R-contracted callable has already integrated over its "
"leg coordinates; declaring the result equal-shell "
"would be vacuous."
)
object.__setattr__(self, "fields", tuple(fields))
object.__setattr__(self, "coupling", coupling)
object.__setattr__(self, "local", local)
object.__setattr__(self, "equal_time", bool(equal_time))
object.__setattr__(self, "already_R_contracted", bool(already_R_contracted))
@property
def n_fields(self) -> int:
return len(self.fields)
@property
def n_physical(self) -> int:
return sum(1 for f in self.fields if f.is_physical)
@property
def n_response(self) -> int:
return sum(1 for f in self.fields if f.is_response)
[docs]
@dataclass
class VertexInstance:
"""A concrete instance of a vertex with freshly-assigned indices.
Created during perturbative expansion when we instantiate copies of S_int.
"""
vertex: Vertex
field_operators: list[FieldOperator]
coupling_symbol: Symbol
spatial_variables: list[str]
component_indices: list[str]
copy_id: int
# For an equal_time non-local vertex: each non-representative leg
# spatial label maps to the canonical representative (the first
# leg) so downstream time integration collapses m legs into one.
# Stored as a tuple of (non_rep, rep) pairs to match the invariant
# used by ``DiagramTerm.equal_time_aliases`` and
# ``SpatialStructure.equal_time_aliases``. Empty tuple for local or
# non-equal-time vertices.
equal_time_aliases: tuple[tuple[str, str], ...] = ()
[docs]
@classmethod
def instantiate(
cls,
vertex: Vertex,
idx_ctx: IndexContext,
copy_id: int,
) -> VertexInstance:
"""Create a concrete vertex instance with fresh indices.
For a local vertex: all fields share one spatial variable.
For a non-local vertex: each field gets its own spatial variable.
"""
operators: list[FieldOperator] = []
comp_indices: list[str] = []
spatial_vars: list[str] = []
if vertex.local:
# All fields share the same spatial argument
shared_spatial = idx_ctx.fresh_spatial_variable()
spatial_vars.append(shared_spatial)
for f in vertex.fields:
if f.is_scalar:
op = FieldOperator(
field=f,
component_index=None,
spatial_arg=shared_spatial,
uid=-1, # placeholder, will be set below
)
else:
comp_idx = idx_ctx.fresh_component_index()
comp_indices.append(comp_idx)
op = FieldOperator(
field=f,
component_index=comp_idx,
spatial_arg=shared_spatial,
uid=-1,
)
operators.append(op)
else:
# Each field gets its own spatial argument
for f in vertex.fields:
spatial = idx_ctx.fresh_spatial_variable()
spatial_vars.append(spatial)
if f.is_scalar:
op = FieldOperator(
field=f,
component_index=None,
spatial_arg=spatial,
uid=-1,
)
else:
comp_idx = idx_ctx.fresh_component_index()
comp_indices.append(comp_idx)
op = FieldOperator(
field=f,
component_index=comp_idx,
spatial_arg=spatial,
uid=-1,
)
operators.append(op)
# Assign unique UIDs
from .fields import _uid_counter
final_operators: list[FieldOperator] = []
for op in operators:
final_op = FieldOperator(
field=op.field,
component_index=op.component_index,
spatial_arg=op.spatial_arg,
uid=next(_uid_counter),
)
final_operators.append(final_op)
# Build coupling symbol with appropriate indices and spatial args
if vertex.local:
coupling_symbol = Symbol(
name=vertex.coupling,
indices=tuple(comp_indices),
spatial_args=(),
)
else:
coupling_symbol = Symbol(
name=vertex.coupling,
indices=tuple(comp_indices),
spatial_args=tuple(spatial_vars),
)
alias_pairs: list[tuple[str, str]] = []
if vertex.equal_time and len(spatial_vars) > 1:
rep = spatial_vars[0]
for var in spatial_vars[1:]:
alias_pairs.append((var, rep))
return cls(
vertex=vertex,
field_operators=final_operators,
coupling_symbol=coupling_symbol,
spatial_variables=spatial_vars,
component_indices=comp_indices,
copy_id=copy_id,
equal_time_aliases=tuple(alias_pairs),
)