home

Matsubara Contours

A thermal frequency sum can be rewritten as a contour integral. Follow which singularities are enclosed, and keep track of the orientation when the contour changes.

Context: thermal field theory contour constructions.


Matsubara Contours

Download

PNG PDF SVG

Code

matsubara-contours.typ (366 lines)
#import "@preview/cetz:0.5.3": canvas, decorations, draw
#import draw: arc, circle, content, line
#let label-size = 12pt
#let paragraph-size = 14pt
#let heading-size = 16pt

#let card_body(title, body, caption) = block(
  width: 100%,
  inset: 12pt,
  radius: 8pt,
  fill: rgb("#cdd3da"),
  breakable: false,
)[
  #text(size: heading-size, weight: "bold", title)
  #v(8pt)
  // Measure unconstrained artwork before scaling, including content wider than its card.
  #layout(size => {
    let artwork = text(size: label-size, body)
    std.scale(size.width / measure(artwork).width * 100%, reflow: true, artwork)
  })
  #v(7pt)
  #text(size: paragraph-size, caption)
]

#let card-grid(columns: 2, ..cards) = layout(size => {
  let rows = cards
    .pos()
    .chunks(columns)
    .map(row => {
      let ratios = row.map(card => {
        let bounds = measure(text(size: label-size, card.at(1)))
        bounds.width / bounds.height
      })
      let available = size.width - 12pt * (row.len() - 1) - 24pt * row.len()
      grid(
        columns: ratios.map(ratio => 24pt + available * ratio / ratios.sum()),
        gutter: 12pt,
        ..row.map(args => card_body(..args)),
      )
    })
  stack(dir: ttb, spacing: 12pt, ..rows)
})

#let takeaway(body) = block(
  width: 100%,
  inset: 12pt,
  radius: 6pt,
  fill: rgb("#c6d8d2"),
  breakable: false,
  text(size: paragraph-size, body),
)

#set page(width: 780pt, height: auto, margin: 22pt, fill: none)
#set text(font: "Avenir Next", size: paragraph-size, fill: rgb("#19324f"))
#set par(leading: 0.55em)

// Hairline tying a label to whatever it names: pole callouts, semi-axis leaders,
// off-diagram vertex captions.
#let leader = (paint: rgb("#78828C"), thickness: 0.5pt)

#let axis-arrow = (mark: (end: "stealth", fill: black, scale: 0.5))

#let dark-blue = blue.darken(30%)

#let contour-stroke = (paint: dark-blue, thickness: 0.8pt)

// Arrowheads at the given fractions along a contour, marking its orientation.
#let contour-marks(..fractions) = (
  end: fractions
    .pos()
    .map(pos => (
      pos: pos,
      symbol: "stealth",
      fill: dark-blue,
      scale: 0.5,
      shorten-to: none,
    )),
)

// Labels start `label-x` right of the axis (past any contour hugging it) plus a small gap.
#let thermal-poles(extent, label-x: 0) = {
  // Dots on the imaginary axis at every non-zero Matsubara frequency i omega_n.
  for n in range(-extent, extent + 1).filter(n => n != 0) {
    let name = "freq-" + str(n)
    circle((0, n), radius: 0.03, fill: black, name: name)
    // 6pt clears the arrowheads on a contour line at label-x
    content((label-x, n), $i omega_#text(size: 0.7em)[#n]$, anchor: "west", padding: 6pt)
  }
  circle((0, 0), radius: 0.03, fill: black, name: "origin")
}

#let function-poles(label-pos) = {
  // Poles of h(p_0), each tied to a shared callout label by a hairline.
  content(label-pos, [poles of $h(p_0)$], name: "poles-label")
  let sites = (
    ((1.5, 3), "west"),
    ((2, -2), "north"),
    ((-3, 1), "south"),
    ((-2, -1.5), "north"),
  )
  for (idx, (pos, anchor)) in sites.enumerate(start: 1) {
    let name = "p" + str(idx)
    circle(pos, radius: 0.05, fill: black, name: name)
    content(name, $p_#idx$, anchor: anchor, padding: 4pt)
    // p3 sits far to the left, so aim its hairline at the label's west edge
    let target = if idx == 3 { "poles-label.west" } else { "poles-label" }
    line(target, name, stroke: leader)
  }
}

#let half-contours(main-radius, y-offset) = {
  // Contour split into a right half C_1 and a left half C_2, each a vertical line
  // running past the poles and closed by a semicircle at infinity.
  let style = (
    stroke: contour-stroke,
    mark: contour-marks(25%, 50%, 75%),
  )
  // C_1 runs down the right of the imaginary axis and back up its semicircle; C_2 mirrors it
  for (sign, start, label, anchor) in (
    (1, -90deg, $C_1$, "south-west"),
    (-1, 90deg, $C_2$, "south-east"),
  ) {
    let x = sign * y-offset
    line((x, sign * main-radius), (x, -sign * main-radius), ..style)
    arc(
      (x, 0),
      radius: main-radius,
      start: start,
      stop: start + 180deg,
      anchor: "origin",
      ..style,
    )
    content((x, -main-radius), text(fill: dark-blue, label), anchor: anchor, padding: 4pt)
  }
}

// Real axis drawn as a branch cut: zigzags flank the origin, with a straight arrow tip.
#let cut-axis(left, right, amplitude: 0.2, segment-length: 0.3) = {
  let (gap, tip) = (0.5, 0.4)
  let zigzag = (amplitude: amplitude, segment-length: segment-length, stroke: 0.8pt)
  line((-gap, 0), (gap, 0), stroke: 0.8pt)
  decorations.zigzag(line((left, 0), (-gap, 0)), ..zigzag)
  decorations.zigzag(line((gap, 0), (right - tip, 0)), ..zigzag)
  line((right - tip, 0), (right, 0), stroke: 0.8pt, ..axis-arrow, name: "x-axis")
}

// === 1  Enclose the thermal poles ===
#let enclose-poles = canvas(length: 1.39cm, {
  let range-xy = 3
  let axis = (..axis-arrow, stroke: 0.5pt)
  let contour = (stroke: dark-blue, mark: (end: "stealth", scale: 0.5))

  line((-range-xy - 1, 0), (range-xy + 1, 0), ..axis, name: "x-axis")
  content("x-axis.end", $"Re"(p_0)$, anchor: "south-east", padding: 2pt)

  line((0, -range-xy - 0.7), (0, range-xy + 0.7), ..axis, name: "y-axis")
  content(
    (rel: (-1, 0), to: "y-axis.95%"),
    $"Im"(p_0)$,
    name: "y-label",
    anchor: "south-east",
    padding: 2pt,
  )
  line("y-axis.98%", "y-label", stroke: leader)

  thermal-poles(range-xy)
  content("origin", [0], anchor: "south-west", padding: (left: 3pt, bottom: 2pt))

  // Contour C hugs the imaginary axis, closed by semicircles beyond the last frequency
  line((1, -range-xy - 0.3), (1, range-xy + 0.3), ..contour, name: "right-line")
  line((-1, range-xy + 0.3), (-1, -range-xy - 0.3), ..contour)
  arc((0, range-xy + 0.6), radius: 1, start: 0deg, stop: 180deg, anchor: "center", ..contour)
  arc((0, -range-xy - 0.5), radius: 1, start: 180deg, stop: 360deg, anchor: "center", ..contour)
  content("right-line.end", text(fill: dark-blue)[$C$], anchor: "south-west", padding: 2pt)

  function-poles((2.75, 1.5))
})

// === 2  Expand, then subtract ===
#let expand-contour = canvas(length: 1.3cm, {
  let y-range = 3
  let main-radius = y-range + 1.5

  line((-main-radius, 0), (main-radius, 0), ..axis-arrow, name: "x-axis")
  content("x-axis.end", $"Re"(p_0)$, anchor: "south-east", padding: 2pt)

  line((0, -main-radius), (0, main-radius), ..axis-arrow, name: "y-axis")
  content("y-axis.97%", $"Im"(p_0)$, anchor: "north-east", padding: 2pt)

  thermal-poles(y-range)
  content("origin", [0], anchor: "south-west", padding: (left: 3pt, bottom: 2pt))

  // Outer contour C, deformable into the four small clockwise pole contours C_1..C_4
  arc(
    (0, 0),
    radius: main-radius,
    start: 0deg,
    stop: 360deg,
    anchor: "origin",
    stroke: dark-blue,
    mark: contour-marks(12.5%, 37.5%, 62.5%, 87.5%),
    name: "main-contour",
  )
  content("main-contour.90%", text(fill: dark-blue)[$C$], anchor: "north-west", padding: 2pt)

  function-poles((2.5, 1.5))

  for idx in range(1, 5) {
    let name = "c" + str(idx)
    arc(
      "p" + str(idx),
      radius: 0.5,
      start: 0deg,
      stop: -360deg,
      anchor: "origin",
      stroke: dark-blue,
      mark: contour-marks(25%, 75%),
      name: name,
    )
    content((rel: (0, 0.8), to: name), text(fill: dark-blue)[$C_#idx$], anchor: "north")
  }
})

// === 3  Separate the half-planes ===
#let half-planes = canvas(length: 1.18cm, {
  let y-range = 3
  let main-radius = y-range + 1.5
  let y-offset = 0.25

  line(
    (-main-radius - y-offset, 0),
    (main-radius + y-offset, 0),
    ..axis-arrow,
    name: "x-axis",
  )
  content("x-axis.end", $"Re"(p_0)$, anchor: "south-east", padding: 2pt)

  line((0, -main-radius), (0, main-radius), ..axis-arrow, name: "y-axis")
  content(((-y-offset, 0), "|-", "y-axis.97%"), $"Im"(p_0)$, anchor: "north-east", padding: 3pt)

  thermal-poles(y-range, label-x: y-offset)
  content((y-offset, 0), [0], anchor: "south-west", padding: 3pt)
  half-contours(main-radius, y-offset)
  function-poles((2.5, 1.5))
})

// === 4  Identify additional singularities ===
#let extra-singularities = canvas(length: 1.4cm, {
  let (x-range, y-range) = (3.5, 3)
  let main-radius = y-range + 0.75
  let y-offset = 0.25

  // Right zigzag stops where the x-axis meets the right arc.
  cut-axis(-x-range - 0.4, y-offset + main-radius, amplitude: 0.15, segment-length: 0.25)
  content("x-axis.end", $"Re"(p_0)$, anchor: "south-east", padding: 2pt)

  line((0, -y-range - 0.7), (0, y-range + 0.7), ..axis-arrow, name: "y-axis")
  content(((-y-offset, 0), "|-", "y-axis.97%"), $"Im"(p_0)$, anchor: "north-east", padding: 3pt)

  thermal-poles(y-range, label-x: y-offset)
  content((y-offset, 0), [0], anchor: "south-west", padding: 3pt)
  half-contours(main-radius, y-offset)

  for (name, pos, label, anchor) in (
    ("pole-e", (x-range / 2, y-range / 4), $E$, "west"),
    ("pole-minus-e", (-x-range / 2, -y-range / 4), $-E$, "east"),
  ) {
    circle(pos, radius: 0.05, fill: black, name: name)
    content(name, label, anchor: anchor, padding: 4pt)
  }
})

// === 5  Follow both sides of a cut ===
#let branch-cut = canvas(length: 2.06cm, {
  let (x-range, y-range) = (3, 1)
  let radius = y-range / 4

  cut-axis(-1.05 * x-range, 1.05 * x-range)
  content("x-axis.end", $"Re"(p_0)$, anchor: "west", padding: 2pt)

  line((0, -y-range), (0, y-range), ..axis-arrow, name: "y-axis")
  content("y-axis.end", $"Im"(p_0)$, anchor: "north-west", padding: 4pt)

  // C_b and its point reflection: each runs in from x = ±x-range, hooks around the
  // origin on a semicircle and back out. Reflection swaps the arc's start and end.
  let leg = (start, end, ..args) => line(
    start,
    end,
    stroke: contour-stroke,
    mark: contour-marks(25%, 75%),
    ..args,
  )
  for sign in (1, -1) {
    let name = "arc" + str(sign)
    arc(
      (sign * radius, 0),
      radius: radius,
      start: 90deg + sign * 180deg,
      stop: 90deg,
      anchor: "arc-center",
      stroke: contour-stroke,
      name: name,
    )
    let (near, far) = if sign == 1 { ("start", "end") } else { ("end", "start") }
    leg((sign * x-range, -sign * radius), name + "." + near)
    leg(name + "." + far, (sign * x-range, sign * radius), name: "outflow" + str(sign))
  }

  content("outflow1.end", text(fill: dark-blue)[$C_b$], anchor: "south-east", padding: 2pt)
})

// === 6  Account for every contribution ===
#let bookkeeping = align(center, stack(
  dir: ttb,
  spacing: 6pt,
  $ sum_(n in ZZ) T h(i omega_n) $,
  $ arrow.b $,
  [*thermal-factor contour integral*],
  $ arrow.b $,
  [*pole residues + cut integrals*],
  text(size: label-size)[with signs fixed by contour orientation],
))

A thermal frequency sum can be rewritten as a contour integral. Follow which singularities are enclosed, and keep track of the orientation when the contour changes.
#v(14pt)
#card-grid(
  (
    [1  Enclose the thermal poles],
    enclose-poles,
    [The blue contour encloses the imaginary-axis Matsubara poles of the thermal factor. Dots away from that axis are poles of the other factor, $h(p_0)$.],
  ),
  (
    [2  Expand, then subtract],
    expand-contour,
    [Expanding the contour also encloses poles of $h$. Clockwise circles subtract them back out. The outer arc vanishes only if the full integrand decays sufficiently fast.],
  ),
  (
    [3  Separate the half-planes],
    half-planes,
    [Contours $C_1$ and $C_2$ run on opposite sides of the imaginary axis. Deformation preserves the integral only while no singularity crosses the path.],
  ),
  (
    [4  Identify additional singularities],
    extra-singularities,
    [Here the example has poles at $+E$ and $-E$ and a real-axis cut. This is a different singularity pattern from the four-pole sketch above.],
  ),
  (
    [5  Follow both sides of a cut],
    branch-cut,
    [The branch-cut contour samples the difference between the limiting values above and below the cut. A cut contributes through this discontinuity.],
  ),
  (
    [6  Account for every contribution],
    bookkeeping,
    [The pictures are a bookkeeping guide, not a universal signed formula. Specify the thermal factor, its residues, and the analytic structure of the complete integrand.],
  ),
)
#v(12pt)
#takeaway[
  #set text(size: 14pt)
  #align(center)[
    *Bosons:* $omega_n = 2 pi n T$ #h(16pt) *Fermions:* $omega_n = (2n+1) pi T$\
    #v(5pt)
    Units: $k_"B" = ℏ = 1$. Check the arc at infinity before discarding it.
  ]
]