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 require polarizability derivatives that phonopy does not produce, so they must be supplied as 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 is still kept in the test suite as an analytic oracle — a linear symmetric triatomic is the one case where IR intensities have a closed form to check the code against — but it is not shown here.

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

IR spectra are conventionally plotted as transmittance, with absorption bands pointing down. Set presentation="transmittance"; the peak shape (Gaussian ↔ Lorentzian) and width are adjustable in the controls pane.

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

Every computed quantity is available per mode, so the raw numbers can be tabulated instead of plotted.

Modeω (cm⁻¹)IR (e²/amu)Raman (a.u.)Character
acoustic0.01.27e-240e+0acoustic
acoustic0.09.98e-190e+0acoustic
acoustic0.05.1e-180e+0acoustic
E127.41.44e-44.17e-1IR
E127.41.44e-44.17e-1IR
A1223.53.3e-186.93e+0Raman/silent
E255.11.07e-22.02e-1IR
E255.11.07e-22.02e-1IR
A1337.05.26e-201.06e+0Raman/silent
A2341.52.02e-10e+0IR
E374.21.34e-13.12e-1IR
E374.21.34e-13.12e-1IR
E435.03.42e-13.59e-1IR
E435.03.42e-13.59e-1IR
A1455.02.14e-193.51e+1Raman/silent
A2482.03.62e-10e+0IR
E691.83.69e-26.79e-1IR
E691.83.69e-26.8e-1IR
A2769.92.13e-10e+0IR
E792.11.49e-17.77e-1IR
E792.11.49e-17.77e-1IR
E1070.61.72e+06.83e-1IR
E1070.61.72e+06.83e-1IR
A21080.71.8e+00e+0IR
A11085.91.1e-181.11e+0Raman/silent
E1148.73.91e-21.41e+0IR
E1148.73.91e-21.41e+0IR
svelte<script lang="ts">
  import { format_num } from '$lib/labels'
  import { convert_frequencies } from '$lib/spectral'
  import { parse_born, parse_phonon_modes, spectrum_from_phonon_data } 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 },
  )
  const labels = raman_data.mode_labels
</script>

<table>
  <thead>
    <tr>
      <th>Mode</th>
      <th>ω (cm⁻¹)</th>
      <th>IR (e²/amu)</th>
      <th>Raman (a.u.)</th>
      <th>Character</th>
    </tr>
  </thead>
  <tbody>
    {#each spectrum.modes as mode (mode.mode_idx)}
      <tr>
        <td>{labels[mode.mode_idx]}</td>
        <td>{format_num(convert_frequencies([mode.frequency], 'cm-1')[0], '.1f')}</td>
        <td>{format_num(mode.ir_intensity, '.2~e')}</td>
        <td>{format_num(mode.raman_activity ?? 0, '.2~e')}</td>
        <td
          >{mode.is_acoustic
            ? 'acoustic'
            : mode.ir_intensity > 1e-12
              ? 'IR'
              : 'Raman/silent'}</td
        >
      </tr>
    {/each}
  </tbody>
</table>

Traps this component deliberately avoids