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:
| Input | Source | Parser |
|---|---|---|
| Frequencies + eigenvectors | phonopy band.yaml / qpoints.yaml / mesh.yaml | parse_phonon_modes |
| Born charges + dielectric | phonopy BORN | parse_born |
| Polarizability derivatives | LEPSILON 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.yamllists 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
normalize_dosis not used. It assumes any frequency above 100 must be in cm⁻¹ and silently divides by 33.36. Vibrational spectra routinely reach 4000 cm⁻¹, so IR/Raman data uses its ownVibrationalSpectrumtype and never touches the DOS normalisation path.apply_gaussian_smearingis not used. It smears values already on a grid and renormalises to preserve their sum, which is not a stick-to-continuum convolution. Broadening goes throughbroaden_peaksfrom$lib/lineshapewith an injected constant (or frequency-dependent) FWHM, so line shapes are area-normalised and the integrated intensity of each mode is preserved.