Advanced Usage

This page covers lower-level API access and more complex calculations.

Direct Wick Contraction

For manual contraction without the perturbative machinery, use wick_contract():

from sft_wick import Field, wick_contract, reset_uid_counter

reset_uid_counter()
phi = Field('phi', 'physical', n_components=3)

ops = [phi('a', 'x'), phi('b', 'y'), phi('c', 'z'), phi('d', 'w')]
expr, pairings = wick_contract(ops)

print(f"Number of pairings: {len(pairings)}")
print(expr.to_latex())
Number of pairings: 3
C_{ab}(x, y) C_{cd}(z, w) + C_{ac}(x, z) C_{bd}(y, w) + C_{ad}(x, w) C_{bc}(y, z)

Single Pair Contraction

contract_pair() contracts exactly two operators:

from sft_wick import Field, contract_pair, reset_uid_counter

reset_uid_counter()
phi = Field('phi', 'physical', n_components=3)
psi = Field('psi', 'response', n_components=3)

# phi-phi -> C
prop = contract_pair(phi('a', 'x'), phi('b', 'y'))
print(prop.to_latex())       # C_{ab}(x, y)

# phi-psi -> R
prop = contract_pair(phi('a', 'x'), psi('b', 'y'))
print(prop.to_latex())       # R_{ab}(x, y)

# psi-psi -> None (vanishes)
prop = contract_pair(psi('a', 'x'), psi('b', 'y'))
print(prop)                  # None

Second-Order Perturbation Theory

Higher orders work identically — just increase the order argument:

from sft_wick import Field, Vertex, Action, compute_moment, reset_uid_counter

reset_uid_counter()
phi = Field('phi', 'physical')
psi = Field('psi', 'response')

v = Vertex(fields=[phi, psi], coupling='g')
action = Action(vertices=[v])

obs = [phi('x'), phi('y')]
result = compute_moment(obs, action, order=2)

for n in range(3):
    n_diagrams = len(result.diagrams_by_order.get(n, []))
    print(f"Order {n}: {n_diagrams} diagrams")
    if n_diagrams > 0:
        print(f"  {result.order(n).to_latex()}")

At second order, the multinomial expansion produces \(S_{\mathrm{int}}^2 / 2!\), and the prefactor \((-1)^2 / 2! = 1/2\) appears.

Diagram Topology Analysis

Access the full networkx.MultiGraph for custom analysis:

for d_info in result.diagrams_by_order[1]:
    fd = d_info.to_feynman_diagram()

    # Topology
    print(f"Loops: {fd.n_loops}")
    print(f"Connected: {fd.is_connected}")

    # Raw networkx access
    g = fd.graph
    print(f"Nodes: {g.number_of_nodes()}")
    print(f"Edges: {g.number_of_edges()}")

    # Node details
    for node, data in g.nodes(data=True):
        print(f"  {node}: {data}")

    # Edge details
    for u, v, key, data in g.edges(keys=True, data=True):
        print(f"  {u} -- {v}: {data['kind']}")

Itô Prescription

By default, ito=True eliminates equal-point response propagators and causal R-loops:

from sft_wick import Field, Vertex, Action, compute_moment, reset_uid_counter

reset_uid_counter()
phi = Field('phi', 'physical')
psi = Field('psi', 'response')

# <psi(x) phi(x)^3> — all at the same point
obs = [psi('x'), phi('x'), phi('x'), phi('x')]

# With Itô (default): R(x,x) = 0, entire result vanishes
result = compute_moment(obs, Action(vertices=[]), order=0)
print(result.order(0).to_latex())  # 0

# Without Itô: 3 R(x,x) C(x,x)
result_raw = compute_moment(
    obs, Action(vertices=[]), order=0, ito=False, response_phase=False
)
print(result_raw.order(0).to_latex())  # 3 R(x, x) C(x, x)

Causal R-loops (e.g. \(R(a,b)\,R(b,a) = 0\)) are also detected and eliminated at any cycle length:

# At first order, some pairings produce R(x,z)R(z,x)
reset_uid_counter()
v = Vertex(fields=[phi, psi], coupling='g')
result_ito = compute_moment(
    [phi('x'), phi('y')], Action([v]), order=1, response_phase=False
)
result_no_ito = compute_moment(
    [phi('x'), phi('y')], Action([v]), order=1,
    ito=False, response_phase=False
)
print(f"Diagrams with Itô: {len(result_ito.diagrams_by_order[1])}")
print(f"Diagrams without:  {len(result_no_ito.diagrams_by_order[1])}")

Response Phase Convention

The MSR convention \(\langle\phi\,\psi\rangle = -\mathrm{i}\,R\) is applied by default (response_phase=True). Each term is multiplied by \((-\mathrm{i})^n\) where n is the number of R propagators:

from sft_wick import apply_response_phase

reset_uid_counter()
obs = [phi('x'), psi('y')]

# With phase (default)
result = compute_moment(obs, Action(vertices=[]), order=0, ito=False)
print(result.order(0).to_latex())  # -1 i R(x, y)

# Without phase
result_raw = compute_moment(
    obs, Action(vertices=[]), order=0, ito=False, response_phase=False
)
print(result_raw.order(0).to_latex())  # R(x, y)

# Manual application
print(apply_response_phase(result_raw.order(0)).to_latex())

Diagram-Based Term Collection

By default (collect_topology=True), terms whose Feynman diagrams are isomorphic (under dummy-variable relabeling) are grouped. Compare the raw vs. collected output:

from sft_wick import Field, Vertex, Action, compute_moment, reset_uid_counter

reset_uid_counter()
phi = Field('phi', 'physical', n_components=3)
psi = Field('psi', 'response', n_components=3)
v = Vertex(fields=[phi, phi, psi], coupling='F')
obs = [phi('a', 'x'), phi('b', 'y')]

# Without diagram collection: many expanded terms at order 2
reset_uid_counter()
r_raw = compute_moment(obs, Action([v]), order=2,
                       response_phase=False, collect_topology=False)

# With diagram collection (default): grouped terms
reset_uid_counter()
r_col = compute_moment(obs, Action([v]), order=2,
                       response_phase=False)

The collected form factors out propagators with canonical component indices and produces sums of permuted coupling tensors (e.g. \(F_{i_0 i_1 i_2} + F_{i_0 i_2 i_1}\)). At second order, spatial-variable relabeling (\(y_0 \leftrightarrow y_1\)) further reduces the number of distinct terms.

Working with Expression Trees

Expressions can be inspected programmatically:

from sft_wick.expressions import Sum, Product, Propagator, Rational, ImaginaryUnit

expr = result.order(1)

if isinstance(expr, Sum):
    print(f"Sum of {len(expr.terms)} terms")
    for term in expr.terms:
        if isinstance(term, Product):
            for factor in term.factors:
                if isinstance(factor, Propagator):
                    print(f"  Propagator: {factor.kind}")
                elif isinstance(factor, Rational):
                    print(f"  Coefficient: {factor.numerator}/{factor.denominator}")
                elif isinstance(factor, ImaginaryUnit):
                    print(f"  Imaginary unit: i")