physics module
The physics module provides reusable source/sink strokes, particle-line patterns, fermion-flow marks, momentum arrows, and label callbacks for draw.
Concepts
Physics Edge Styles
physics.style(..) returns source-style, sink-style, and edge-label callbacks for draw. The default source/sink strokes use darker/lighter halves to encode the graph source/sink split. Fermion map entries marked with fermion-flow receive one particle-flow arrow on the main edge using mark-orientation: "edge"; paired edges follow edge.orientation, dangling half edges place the arrow in the middle of the visible half edge, and undirected edges omit the arrow. This is independent of momentum arrows.
Set momentum-arrows: true to draw centered parallel arrow decorations while the main edge remains connected to the nodes. The arrows flow from source to sink independently of the DOT dir/orientation value; there is one momentum marker per edge, even though the base edge is internally styled as source and sink halves. On paired edges the marker lives on the sink half; on dangling edges it lives on the existing source or sink half-edge. The decoration defaults to a plain black 0.55pt stroke and a scaled CeTZ "barbed" arrowhead, so it does not inherit the base particle stroke. The arrow length is capped by both momentum-arrow-length and momentum-arrow-ratio, so the shorter one wins. When the layout provides an edge-label position, the arrow offset is signed so the momentum marker is drawn on the same side of the edge as that label. Use momentum-arrow-offset to set the normal displacement. momentum-arrow-mark: auto uses the default barbed marker, momentum-arrow-mark: none draws only the momentum line, and any other value overrides the CeTZ mark style, for example "stealth" or (end: "barbed",. Only the
scale: 1.6)end mark is kept so source/sink splitting cannot create a second momentum head.
Optional labels can be built from edge metadata with show-edge-index, show-half-edge-index, show-particle, and, for explicit momentum fields, show-momentum. show-half-edge-index only emits a value for dangling half edges.
#import "../src/lib.typ": draw, graph, layout, physics
#import graph: build, dot, edge, edges, node, nodes, parse, sink, source
#let g = build({
node(<a>)
node(<c>)
edge(
source(<a>),
<fermion-main>,
sink(<c>),
id: 7,
particle: "fermion",
)
edge(
source(<c>),
<fermion-out>,
id: 8,
particle: "fermion",
)
},
name: "physics",
)
#let callbacks = physics.style(
momentum-arrows: true,
show-edge-index: true,
show-particle: true,
)
#draw(
layout(g),
source-style: callbacks.source-style,
sink-style: callbacks.sink-style,
edge-label: callbacks.edge-label,
)Set edge-offset on draw, or offset in a source-style/sink-style dictionary, to draw a fitted parallel path. Style callbacks may also return an array of dictionaries; the layers are drawn in order on the same graph edge, so parallel strokes do not require duplicate graph edges.
#let base-style(edge) = (
stroke: (paint: gray, thickness: 0.7pt, cap: "round"),
)
#let offset-style(edge) = (
offset: 0.18,
length: 1.4,
ratio: 0.5,
resolve-length: "min",
stroke: (paint: rgb("#2f6f4e"), thickness: 1pt, cap: "round"),
)
#let source-style(edge) = (base-style(edge), offset-style(edge))
#let sink-style(edge) = (base-style(edge), offset-style(edge) + (mark: (end: ">")))The parallel path is applied to the base edge geometry before patterns and other decorations; node outsets then trim the shifted path, so shifted paths still start and end outside fitted node circles. Add edge-length or length to center-trim the shifted path to a fixed arc length, and add edge-ratio or ratio to cap it by a fraction of the base edge length. edge-resolve-length
resolve-lengthdecides how to combine both limits"min"/"shorter"(default),"max"/"longer",
"length"/"fixed", "ratio"/"relative", "none"/"full", or a function receiving (base-length, length, ratio). Set offset-side: "label" on an offset layer to choose the sign of offset so the layer is drawn on the same side of the curve as the edge label.
Data defaults are also the global styling hook for all sources, sinks, nodes, and edges. More specific data can be added with captured named arguments on node, edge, source, and sink items, or by running graph.map/graph.eval-fields after construction. This keeps evaluated Typst values in the native data channel instead of adding renderer-specific eval fields to the Rust topology spec:
#let g = build({
node(<a>)
node(<c>)
edge(
source(<a>),
<a-c>,
sink(<c>),
label: [a-c],
kind: "highlight",
)
},
name: "demo",
default-source-data: (style: (stroke: red + 0.5pt)),
default-sink-data: (style: (stroke: blue + 0.5pt)),
)
#let callbacks = physics.style()
#draw(layout(g), source-style: callbacks.source-style, sink-style: callbacks.sink-style)graph.build also accepts comma-separated items:
#let g = build(
node(<a>),
node(<b>),
edge(
source(<a>, compass: "e"),
<ab>,
sink(<b>, compass: "w"),
label: [ab],
statements: (color: "0055ff", label: "ab"),
),
name: "demo",
statements: (full_num: "x + y"),
default-node-statements: (shape: "circle"),
default-edge-statements: (
color: "000000",
display-label: "$#label$",
),
)physics
- stroke-style()
- source-stroke()
- sink-stroke()
- particle-name()
- edge-entry()
- text-value()
- eval-expression()
- label-content()
- style-dict()
- momentum-value()
- edge-index()
- dangling-half-edge-index()
- source-style()
- sink-style()
- edge-label()
- style()
Variables
- massive
- massless
- dashed
- dotted
- fermion-arrow-mark
- fermion-flow
- wave
- coil
- zigzag
- default-edge
- default-map
- momentum-arrow-defaults
stroke-style
Build a rounded CeTZ stroke dictionary accepted by linnest.draw.
Parameters
source-stroke
Source-half stroke helper.
Parameters
sink-stroke
Sink-half stroke helper. The default lightening makes the two halves encode the graph’s source/sink split without requiring arrowheads.
Parameters
particle-name
Return an edge’s particle name, stripping the quotes often present in DOT statement values.
Parameters
edge-entry
Return the style-map entry for an edge.
Parameters
text-value
Convert DOT-ish values to plain text.
Parameters
eval-expression
Evaluate a string-valued style expression in the edge scope.
Parameters
label-content
Convert a style label value to content. In mode: "eval", string values are evaluated in the edge scope.
Parameters
style-dict
Convert a style value to a dictionary. In mode: "eval", string values are evaluated in the edge scope.
Parameters
momentum-value
Return the momentum field used by optional edge labels.
Parameters
edge-index
Return the edge index used by optional edge labels. The DOT id statement wins over the renderer-local eid.
Parameters
dangling-half-edge-index
Return the exposed half-edge index for dangling edges only.
Parameters
source-style
Style callback for the source half edge.
momentum-arrows: true adds one centered black CeTZ-mark decoration while the main edge remains drawn with its normal particle style.
Parameters
sink-style
Style callback for the sink half edge.
momentum-arrows: true adds one centered black CeTZ-mark decoration toward the sink node, so momentum arrows always flow from source to sink independently of edge.orientation.
Parameters
edge-label
Edge-label callback.
By default this preserves data display-label or label, then explicit display-label, label, and particle-map labels. Set any show-* option to build a label from selected metadata instead:
#let callbacks = physics.style(
momentum-arrows: true,
show-edge-index: true,
show-particle: true,
)Parameters
style
Bundle source-style, sink-style, and edge-label callbacks with shared options for linnest.draw.
#let g = build({
node(<a>,pos:pos(y:0))
node(<b>,pos:pos(y:0))
edge(source(<a>), <g-edge>, sink(<b>), particle: "g")
edge(source(<a>), <g-edge2>, sink(<b>), particle: "g")
edge(source(<a>), <g-edge3>, sink(<b>), particle: "g")
},
name: "physics",
)
#let a = ```dot
digraph {
0 [dod="-100" num="1"];
1 [dod="-100" num="1"];
2 [dod="-100" num="1"];
3 [dod="-100" num="1"];
4 [dod="-100" num="1"];
5 [dod="-100" num="1"];
6 [dod="-100" num="1"];
exte0 [style=invis];
exte0 -> 2:0 [id=0 dod="-100" lmb_rep="P(0,a___)" num="1" particle="d" pin="x:@-left"];
exte1 [style=invis];
exte1 -> 1:1 [id=1 dir=back dod="-100" lmb_rep="P(1,a___)" num="1" particle="d~" pin="x:@-left"];
exte2 [style=invis];
exte2 -> 1:2 [id=2 dir=none dod="-100" lmb_rep="P(2,a___)" mass="0" num="1" particle="H" pin="x:@-left"];
exte3 [style=invis];
exte3 -> 2:3 [id=3 dir=none dod="-100" is_dummy="true" lmb_rep="0" mass="0" num="1" particle="H" pin="x:@-left"];
exte4 [style=invis];
6:4 -> exte4 [id=4 dir=none dod="-100" lmb_rep="P(3,a___)" mass="0" num="1" particle="H" pin="x:@+right"];
exte5 [style=invis];
1:5 -> exte5 [id=5 dir=none dod="-100" is_dummy="true" lmb_rep="0" mass="0" num="1" particle="H" pin="x:@+right"];
exte6 [style=invis];
5:6 -> exte6 [id=6 dir=none dod="-100" lmb_rep="P(4,a___)" num="1" particle="H" pin="x:@+right"];
exte7 [style=invis];
4:7 -> exte7 [id=7 dir=none dod="-100" lmb_rep="P(5,a___)" num="1" particle="H" pin="x:@+right"];
exte8 [style=invis];
3:8 -> exte8 [id=8 dir=none dod="-100" lmb_rep="-1*P(3,a___)+-1*P(4,a___)+-1*P(5,a___)+P(0,a___)+P(1,a___)+P(2,a___)" num="1" particle="H" pin="x:@+right"];
0:9 -> 1:10 [id=9 dod="-100" lmb_id="1" lmb_rep="K(1,a___)" num="1" particle="t"];
0:11 -> 2:12 [id=10 dir=none dod="-100" lmb_rep="-1*P(0,a___)+K(0,a___)" num="1" particle="g"];
5:13 -> 0:14 [id=11 dod="-100" lmb_rep="-1*P(0,a___)+K(0,a___)+K(1,a___)" num="1" particle="t"];
6:24 -> 1:18 [id=12 dir=none dod="-100" lmb_rep="-1*P(3,a___)+K(0,a___)" mass="((Q(2,spenso::cind(0)))^2+(Q(2,spenso::cind(1)))^2*-1+(Q(2,spenso::cind(2)))^2*-1+(Q(2,spenso::cind(3)))^2*-1)^(1/2)" num="1" particle="g"];
1:15 -> 4:16 [id=13 dod="-100" lmb_rep="-1*P(3,a___)+K(0,a___)+K(1,a___)+P(1,a___)+P(2,a___)" num="1" particle="t"];
2:17 -> 6:23 [id=14 dod="-100" lmb_id="0" lmb_rep="K(0,a___)" num="1" particle="d"];
4:19 -> 3:20 [id=15 dod="-100" lmb_rep="-1*P(3,a___)+-1*P(5,a___)+K(0,a___)+K(1,a___)+P(1,a___)+P(2,a___)" num="1" particle="t"];
3:21 -> 5:22 [id=16 dod="-100" lmb_rep="-1*P(0,a___)+K(0,a___)+K(1,a___)+P(4,a___)" num="1" particle="t"];
}
```
#let (source-style, sink-style, edge-label, ..callbacks) = physics.style(momentum-arrows: true, show-edge-index: true)
#draw(
layout(g,beta: 100),
source-style: source-style,
sink-style: sink-style,
edge-label: edge-label,
)
#let g = parse(a.text).at(0)
#draw(
layout(layout(g,beta: 100,epochs:300,steps:200),subgraph: subgraph.bits(g,(false,false,false,false,false,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true))),
source-style: source-style,
sink-style: sink-style,
edge-label: edge-label,
)Parameters
massive
Conventional massive-particle stroke thickness.
massless
Conventional massless-particle stroke thickness.
dashed
Dash pattern used for scalar-style lines.
dotted
Dotted dash style used for ghost-style lines.
fermion-arrow-mark
CeTZ marker used for fermion particle-flow arrows on the main edge.
fermion-flow
Mark an edge-map entry as a fermion so the main edge receives one particle-flow arrow that follows edge.orientation. This is separate from optional momentum arrows.
wave
Photon-style wave pattern.
coil
Gluon-style coil pattern.
zigzag
Weak-boson-style zigzag pattern.
default-edge
Fallback style entry used when an edge has no known particle type.
default-map
Small built-in particle map for standalone physics diagrams. GammaLoop generated edge-style.typ files pass their model-specific map to style.
momentum-arrow-defaults
Default style for source-to-sink momentum arrow layers.