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:
| 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 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 |
|---|---|---|---|---|
| acoustic | 0.0 | 1.27e-24 | 0e+0 | acoustic |
| acoustic | 0.0 | 9.98e-19 | 0e+0 | acoustic |
| acoustic | 0.0 | 5.1e-18 | 0e+0 | acoustic |
| E | 127.4 | 1.44e-4 | 4.17e-1 | IR |
| E | 127.4 | 1.44e-4 | 4.17e-1 | IR |
| A1 | 223.5 | 3.3e-18 | 6.93e+0 | Raman/silent |
| E | 255.1 | 1.07e-2 | 2.02e-1 | IR |
| E | 255.1 | 1.07e-2 | 2.02e-1 | IR |
| A1 | 337.0 | 5.26e-20 | 1.06e+0 | Raman/silent |
| A2 | 341.5 | 2.02e-1 | 0e+0 | IR |
| E | 374.2 | 1.34e-1 | 3.12e-1 | IR |
| E | 374.2 | 1.34e-1 | 3.12e-1 | IR |
| E | 435.0 | 3.42e-1 | 3.59e-1 | IR |
| E | 435.0 | 3.42e-1 | 3.59e-1 | IR |
| A1 | 455.0 | 2.14e-19 | 3.51e+1 | Raman/silent |
| A2 | 482.0 | 3.62e-1 | 0e+0 | IR |
| E | 691.8 | 3.69e-2 | 6.79e-1 | IR |
| E | 691.8 | 3.69e-2 | 6.8e-1 | IR |
| A2 | 769.9 | 2.13e-1 | 0e+0 | IR |
| E | 792.1 | 1.49e-1 | 7.77e-1 | IR |
| E | 792.1 | 1.49e-1 | 7.77e-1 | IR |
| E | 1070.6 | 1.72e+0 | 6.83e-1 | IR |
| E | 1070.6 | 1.72e+0 | 6.83e-1 | IR |
| A2 | 1080.7 | 1.8e+0 | 0e+0 | IR |
| A1 | 1085.9 | 1.1e-18 | 1.11e+0 | Raman/silent |
| E | 1148.7 | 3.91e-2 | 1.41e+0 | IR |
| E | 1148.7 | 3.91e-2 | 1.41e+0 | IR |
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
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.