IR and Raman Spectra

The IrRamanSpectrum component renders vibrational spectra from phonon modes at Γ. Infrared intensities are computed here from Born effective charges and phonon eigenvectors. Raman activities are not computed from eigenvectors. They need polarizability derivatives that phonopy does not produce, so supply per-mode tensors or precomputed activities.

Where the data comes from

Three inputs are needed:

InputSourceParser
Frequencies + eigenvectorsphonopy band.yaml / qpoints.yaml / mesh.yamlparse_phonon_modes
Born charges + dielectricphonopy BORNparse_born
Polarizability derivativesLEPSILON finite differences (VASP, phonopy-spectroscopy)supplied directly as raman_tensors

Eigenvectors follow phonopy’s convention: eigenvectors of the mass-weighted dynamical matrix, normalised to sum |e|² = 1, so the physical displacement of atom κ is e_κ / sqrt(M_κ). parse_phonon_modes verifies the normalisation and rejects files that violate it.

NaCl is real first-principles data: Γ-point modes and Born effective charges from PhononDB (PBEsol, dataset, DOI 10.48505/nims.4197), solved from the published force constants with phonopy. Its TO mode lands at 168.5 cm⁻¹ against ~164 cm⁻¹ measured.

α-quartz is real too: Γ modes, Born charges and polarizability derivatives from the Phonopy-Spectroscopy VASP example (MIT, © 2017 Jonathan Michael Skelton). Its Raman-Tensors.yaml lists only the Raman-active modes, so the four IR-active A₂ modes and the three acoustic branches are zero-filled to give one tensor per mode.

Every fixture rendered on this page is first-principles output. A synthetic CO₂ fixture remains in the test suite as an analytic oracle (a linear symmetric triatomic is the one case with a closed-form IR intensity check), but it is not shown here.

Explore bands, peaks, and atomic motion together

PhononModeExplorer connects every selectable band point or IR/Raman stick to a 3D animation of the corresponding complex eigenvector. Away from Γ it applies the Bloch phase across a configurable diagonal supercell and renders the resulting coordinates without periodic wrapping. The trajectory controls provide playback, frame scrubbing, fullscreen, and video export.

For the visualization-first experience, open the dedicated Phonon Mode Explorer. The embedded example below exposes the same public component alongside its source.

This compact NaCl Γ→X path was generated from the same published PhononDB force constants as the Γ-only example below. Click a band, an IR stick, or a row in the mode list to change the animation.

svelte<script lang="ts">
  import {
    PhononModeExplorer,
    parse_born,
    parse_phonon_modes,
    spectrum_from_phonon_data,
  } from '$lib/spectral'
  import born_file from '$site/phonons/ir-raman/NaCl.BORN?raw'
  import band_yaml from '$site/phonons/ir-raman/NaCl-Gamma-X-band.yaml?raw'

  const modes = parse_phonon_modes(band_yaml)
  const dataset = { modes, spectrum: spectrum_from_phonon_data(modes, parse_born(born_file)) }
</script>

<PhononModeExplorer {dataset} style="height: 680px" />

PhononModeExplorer is a typed renderer. Parse or fetch files at the application boundary, then pass the resulting modes and optional spectrum together as one dataset.

Infrared spectrum of NaCl

Rocksalt NaCl has one triply degenerate T₁u optical mode. Its IR intensity is analytic, Z*²/μ with μ the reduced mass, which makes it a good end-to-end check of the pipeline.

svelte<script lang="ts">
  import { IrRamanSpectrum } from '$lib/spectral'
  import { parse_born, parse_phonon_modes, spectrum_from_phonon_data } from '$lib/spectral'
  import born_file from '$site/phonons/ir-raman/NaCl.BORN?raw'
  import yaml_file from '$site/phonons/ir-raman/NaCl-gamma.yaml.gz?raw'

  const spectrum = spectrum_from_phonon_data(
    parse_phonon_modes(yaml_file),
    parse_born(born_file),
  )
</script>

<IrRamanSpectrum {spectrum} fwhm={8} style="height: 400px" />

The three acoustic modes are identified at Γ and excluded from the stick spectrum: with sum_κ Z*_κ = 0 and every atom displaced identically, their dipole derivative cancels exactly.

IR and Raman selection rules in α-quartz

α-quartz (SiO₂, P3₁2₁) has point group 32, where A₁ modes are Raman-active but IR-silent, A₂ modes are IR-active but Raman-silent, and the doubly degenerate E modes are both. Switch between the two spectra in the controls pane (hover the plot to reveal it): four A₂ modes carry IR intensity and no Raman activity, and they are the only ones missing from the Raman trace.

svelte<script lang="ts">
  import { IrRamanSpectrum } from '$lib/spectral'
  import { parse_born, parse_phonon_modes, spectrum_from_phonon_data } from '$lib/spectral'
  import type { SpectrumKind } from '$lib/spectral'
  import raman_data from '$site/phonons/ir-raman/SiO2-raman-tensors.json.gz'
  import born_file from '$site/phonons/ir-raman/SiO2.BORN?raw'
  import yaml_file from '$site/phonons/ir-raman/SiO2-gamma.yaml.gz?raw'

  const spectrum = spectrum_from_phonon_data(
    parse_phonon_modes(yaml_file),
    parse_born(born_file),
    { raman_tensors: raman_data.raman_tensors },
  )

  let kind = $state<SpectrumKind>('ir')
</script>

<label style="display: block; margin-bottom: 0.5em">
  Spectrum:
  <select bind:value={kind}>
    <option value="ir">Infrared</option>
    <option value="raman">Raman</option>
  </select>
</label>

<IrRamanSpectrum {spectrum} bind:kind fwhm={25} style="height: 400px" />

Transmittance presentation

Set presentation="transmittance" to flip IR bands downwards. fwhm sets the peak width, shape_factor the pseudo-Voigt mixing (0 = Gaussian, 1 = Lorentzian).

svelte<script lang="ts">
  import { IrRamanSpectrum } from '$lib/spectral'
  import { parse_born, parse_phonon_modes, spectrum_from_phonon_data } from '$lib/spectral'
  import born_file from '$site/phonons/ir-raman/SiO2.BORN?raw'
  import yaml_file from '$site/phonons/ir-raman/SiO2-gamma.yaml.gz?raw'

  const spectrum = spectrum_from_phonon_data(
    parse_phonon_modes(yaml_file),
    parse_born(born_file),
  )
</script>

<IrRamanSpectrum
  {spectrum}
  presentation="transmittance"
  fwhm={30}
  shape_factor={1}
  style="height: 400px"
/>

Mode table

Selected computed quantities are available per mode, so the raw numbers can be tabulated instead of plotted.

Mode ω (cm⁻¹) IR (e²/amu) Raman (a.u.) Character
acoustic 0.0 1.27e-24 0 acoustic
acoustic 0.0 9.98e-19 0 acoustic
acoustic 0.0 5.1e-18 0 acoustic
E 127.4 1.44e-4 4.17e-1 IR + Raman
E 127.4 1.44e-4 4.17e-1 IR + Raman
A1 223.5 3.3e-18 6.93e+0 Raman
E 255.1 1.07e-2 2.02e-1 IR + Raman
E 255.1 1.07e-2 2.02e-1 IR + Raman
A1 337.0 5.26e-20 1.06e+0 Raman
A2 341.5 2.02e-1 0 IR
E 374.2 1.34e-1 3.12e-1 IR + Raman
E 374.2 1.34e-1 3.12e-1 IR + Raman
E 435.0 3.42e-1 3.59e-1 IR + Raman
E 435.0 3.42e-1 3.59e-1 IR + Raman
A1 455.0 2.14e-19 3.51e+1 Raman
A2 482.0 3.62e-1 0 IR
E 691.8 3.69e-2 6.79e-1 IR + Raman
E 691.8 3.69e-2 6.8e-1 IR + Raman
A2 769.9 2.13e-1 0 IR
E 792.1 1.49e-1 7.77e-1 IR + Raman
E 792.1 1.49e-1 7.77e-1 IR + Raman
E 1070.6 1.72e+0 6.83e-1 IR + Raman
E 1070.6 1.72e+0 6.83e-1 IR + Raman
A2 1080.7 1.8e+0 0 IR
A1 1085.9 1.1e-18 1.11e+0 Raman
E 1148.7 3.91e-2 1.41e+0 IR + Raman
E 1148.7 3.91e-2 1.41e+0 IR + Raman
svelte<script lang="ts">
  import {
    convert_frequencies,
    parse_born,
    parse_phonon_modes,
    spectrum_from_phonon_data,
  } from '$lib/spectral'
  import { HeatmapTable, type Label } from '$lib/table'
  import raman_data from '$site/phonons/ir-raman/SiO2-raman-tensors.json.gz'
  import born_file from '$site/phonons/ir-raman/SiO2.BORN?raw'
  import yaml_file from '$site/phonons/ir-raman/SiO2-gamma.yaml.gz?raw'

  const spectrum = spectrum_from_phonon_data(
    parse_phonon_modes(yaml_file),
    parse_born(born_file),
    { raman_tensors: raman_data.raman_tensors },
  )
  const frequencies = convert_frequencies(
    spectrum.modes.map((mode) => mode.frequency),
    `cm-1`,
  )
  const mode_data = spectrum.modes.map((mode, idx) => {
    const activity = [
      mode.ir_intensity > 1e-12 && `IR`,
      (mode.raman_activity ?? 0) > 1e-12 && `Raman`,
    ]
      .filter(Boolean)
      .join(` + `)
    return {
      mode: raman_data.mode_labels[mode.mode_idx],
      frequency: frequencies[idx],
      ir_intensity: mode.ir_intensity,
      raman_activity: mode.raman_activity ?? 0,
      character: mode.is_acoustic ? `acoustic` : activity || `silent`,
    }
  })
  const mode_columns = [
    { label: `Mode`, key: `mode`, color_scale: null, sticky: true },
    { label: `ω (cm⁻¹)`, key: `frequency`, format: `.1f` },
    { label: `IR (e²/amu)`, key: `ir_intensity`, format: `.2~e`, scale_type: `log` },
    { label: `Raman (a.u.)`, key: `raman_activity`, format: `.2~e`, scale_type: `log` },
    { label: `Character`, key: `character`, color_scale: null },
  ] satisfies Label[]
</script>

<HeatmapTable data={mode_data} columns={mode_columns} />

Traps this component deliberately avoids