ADF Optimization and Vibrational Analysis of Formaldehyde

../_images/ir_spectrum_6b6fd8e4.png

Broadened IR spectrum of optimized formaldehyde.

Requires: AMS2026 or later

Related documentation

Calculation

Formaldehyde was built from the SMILES string C=O, symmetrized, and optimized as a neutral closed-shell molecule. The optimized structure has C(2V) symmetry. ADF used PBE-D3(BJ), the all-electron DZP basis, and default numerical and SCF settings. Harmonic normal modes and IR intensities were calculated at the final optimized geometry.

Job directory: 01-run_workdir/formaldehyde_adf_pbe_dzp

Provenance: Combined ADF geometry optimization, harmonic frequencies, IR intensities, and PES-point characterization.

Optimized geometry

Quantity

Value

Unit

C=O

1.212143

angstrom

C-H(1)

1.115562

angstrom

C-H(2)

1.115562

angstrom

mean C-H

1.115562

angstrom

H-C-H

115.943895

degree

Optimized formaldehyde structure

Minimum verification

AMS PES-point classification: local minimum.

Negative harmonic frequencies: 0.

The optimized formaldehyde structure is a local minimum. AMS classified the PES point as local minimum, and all 6 vibrational frequencies are positive.

Vibrational modes

The assignments use changes in C=O distance, the two C-H distances, H-C-H angle, and displacement normal to the molecular plane. They are qualitative harmonic-mode descriptions.

Mode

Frequency (cm^-1)

IR intensity (km mol^-1)

Symmetry

Qualitative assignment

1

1157.06

4.682

B1

CH2 out-of-plane bend

2

1226.38

8.786

B2

CH2 in-plane rocking bend

3

1487.73

14.647

A1

H-C-H scissoring bend

4

1743.28

88.378

A1

C=O stretch

5

2797.14

60.003

A1

symmetric C-H stretch

6

2850.66

133.912

B2

asymmetric C-H stretch

Broadened IR spectrum

The blue curve applies Gaussian broadening with a width of 20 cm^-1 to the calculated sticks. Red lines mark the unbroadened modes. The wavenumber axis follows the usual IR convention.

Broadened harmonic IR spectrum

Normal-mode displacement pictures

The maximum atomic displacement in each outer structure is 0.25 angstrom. All views use along_pca3.

Mode 1: 1157.06 cm^-1, B1

Assignment: CH2 out-of-plane bend. The left and right structures use equal displacements along the normal coordinate.

Mode 1 displacement grid

Mode 2: 1226.38 cm^-1, B2

Assignment: CH2 in-plane rocking bend. The left and right structures use equal displacements along the normal coordinate.

Mode 2 displacement grid

Mode 3: 1487.73 cm^-1, A1

Assignment: H-C-H scissoring bend. The left and right structures use equal displacements along the normal coordinate.

Mode 3 displacement grid

Mode 4: 1743.28 cm^-1, A1

Assignment: C=O stretch. The left and right structures use equal displacements along the normal coordinate.

Mode 4 displacement grid

Mode 5: 2797.14 cm^-1, A1

Assignment: symmetric C-H stretch. The left and right structures use equal displacements along the normal coordinate.

Mode 5 displacement grid

Mode 6: 2850.66 cm^-1, B2

Assignment: asymmetric C-H stretch. The left and right structures use equal displacements along the normal coordinate.

Mode 6 displacement grid

AMS input

Properties
  NormalModes yes
  PESPointCharacter yes
End

Task GeometryOptimization

System
  Atoms
              C       0.0000000000       0.0000000000       0.0337596501
              O       0.0000000000       0.0000000000      -1.1856950809
              H       0.0000000000       0.9391319492       0.5759677154
              H       0.0000000000      -0.9391319492       0.5759677154
  End
  BondOrders
     1 2 2.0
     1 3 1.0
     1 4 1.0
  End
End

Engine adf
  Basis
    Core None
    Type DZP
  End
  XC
    Dispersion GRIMME3 BJDAMP
    GGA PBE
  End
EndEngine

Conclusion

The optimized formaldehyde structure is a local minimum. AMS classified the PES point as local minimum, and all 6 vibrational frequencies are positive. The optimized bond metrics are C=O 1.212143 angstrom, mean C-H 1.115562 angstrom, and H-C-H 115.9439 degrees.

Prompts and Python scripts

Prompt (instruction for AI agent)
Use $ams2026

Optimize formaldehyde and calculate its vibrational frequencies with ADF.

Build formaldehyde from SMILES C=O and symmetrize. Run an ADF geometry
optimization with a modest GGA functional, a small-to-medium basis set,
and dispersion.

Verify that the optimized structure is a minimum with no imaginary
frequencies. Report the optimized C=O and C-H bond lengths and the H-C-H angle.

In the report, include a table of vibrational frequencies, IR intensities,
symmetries, and qualitative mode assignments. Plot a simple broadened IR
spectrum from the calculated frequencies and intensities.

For each normal mode, use plot_image_grid along_pca3 to show three pictures
side-by-side where you have displaced-in-negative-direction, optimized
structure, displaced-in-positive-direction.
01-run.py
#!/usr/bin/env amspython
from __future__ import annotations

from pathlib import Path

from scm.base import ChemicalSystem, InputParser
from scm.plams import AMSJob, Settings, finish, init


def build_formaldehyde() -> tuple[ChemicalSystem, str]:
    """Build formaldehyde from the requested SMILES and impose molecular symmetry."""
    system = ChemicalSystem.from_smiles("C=O")
    point_group = system.symmetrize_molecule(tolerance=0.10)
    return system, point_group


def adf_settings() -> Settings:
    settings = Settings()
    settings.input.ams.Task = "GeometryOptimization"
    settings.input.ams.Properties.NormalModes = "Yes"
    settings.input.ams.Properties.PESPointCharacter = "Yes"
    settings.input.adf.Basis.Type = "DZP"
    settings.input.adf.Basis.Core = "None"
    settings.input.adf.XC.GGA = "PBE"
    settings.input.adf.XC.Dispersion = "GRIMME3 BJDAMP"
    return settings


def main() -> None:
    system, point_group = build_formaldehyde()
    settings = adf_settings()
    job = AMSJob(molecule=system, settings=settings, name="formaldehyde_adf_pbe_dzp")

    input_text = job.get_input()
    InputParser().to_dict("ams", input_text)
    Path("validated_input.in").write_text(input_text, encoding="utf-8")
    print(f"Built formaldehyde from SMILES C=O and symmetrized it to {point_group}.")
    print("Validated the serialized AMS/ADF input against the installed input definitions.")

    init(folder="01-run_workdir")
    results = job.run()
    if not job.ok():
        raise RuntimeError(f"AMS job did not finish successfully: {results.job.path}")
    print(f"Completed job: {job.path}")
    finish()


if __name__ == "__main__":
    main()
report.py
#!/usr/bin/env amspython
from __future__ import annotations

from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scm.plams import AMSJob, plot_image_grid, view


ROOT = Path(__file__).resolve().parent
TABLES = ROOT / "tables"
FIGURES = ROOT / "figures"
JOB_NAME = "formaldehyde_adf_pbe_dzp"


def latest_job_path() -> Path:
    workdirs = [path for path in ROOT.glob("01-run_workdir*") if path.is_dir()]
    if not workdirs:
        raise FileNotFoundError("No 01-run_workdir directory was found")
    latest = max(workdirs, key=lambda path: path.stat().st_mtime)
    job_path = latest / JOB_NAME
    if not job_path.is_dir():
        raise FileNotFoundError(f"Expected AMS job directory: {job_path}")
    return job_path


def atom_indices(system) -> tuple[int, int, list[int]]:
    carbon = [i for i, atom in enumerate(system.atoms) if atom.symbol == "C"]
    oxygen = [i for i, atom in enumerate(system.atoms) if atom.symbol == "O"]
    hydrogens = [i for i, atom in enumerate(system.atoms) if atom.symbol == "H"]
    if len(carbon) != 1 or len(oxygen) != 1 or len(hydrogens) != 2:
        raise ValueError("The optimized system does not have the expected CH2O composition")
    return carbon[0], oxygen[0], hydrogens


def symmetry_labels(job: AMSJob, count: int) -> list[str]:
    raw = job.results.readrkf("Vibrations", "IrReps", file="engine")
    if isinstance(raw, str):
        labels = raw.split()
    else:
        labels = [str(value).strip() for value in np.asarray(raw).reshape(-1)]
    if len(labels) != count:
        raise ValueError(f"Found {len(labels)} symmetry labels for {count} modes")
    return labels


def internal_derivatives(system, mode: np.ndarray) -> dict[str, float]:
    c_idx, o_idx, h_idx = atom_indices(system)
    step = 0.01
    plus = system.copy()
    minus = system.copy()
    plus.coords[:] = np.asarray(system.coords) + step * mode
    minus.coords[:] = np.asarray(system.coords) - step * mode

    def central(value_plus: float, value_minus: float) -> float:
        return (value_plus - value_minus) / (2.0 * step)

    d_co = central(plus.get_distance(c_idx, o_idx), minus.get_distance(c_idx, o_idx))
    d_ch = [
        central(plus.get_distance(c_idx, h), minus.get_distance(c_idx, h))
        for h in h_idx
    ]
    d_angle = central(
        plus.get_angle(h_idx[0], c_idx, h_idx[1], unit="degree"),
        minus.get_angle(h_idx[0], c_idx, h_idx[1], unit="degree"),
    )
    coords = np.asarray(system.coords)
    molecular_normal = np.cross(coords[o_idx] - coords[c_idx], coords[h_idx[0]] - coords[c_idx])
    molecular_normal /= np.linalg.norm(molecular_normal)
    oop = sum(abs(np.dot(mode[h] - mode[c_idx], molecular_normal)) for h in h_idx)
    return {"co": d_co, "ch1": d_ch[0], "ch2": d_ch[1], "angle": d_angle, "oop": oop}


def assign_modes(frequencies: np.ndarray, modes: np.ndarray, system) -> list[str]:
    metrics = [internal_derivatives(system, mode) for mode in modes]
    assignments = [""] * len(frequencies)
    high = [i for i, frequency in enumerate(frequencies) if frequency > 2200.0]
    low = [i for i in range(len(frequencies)) if i not in high]

    for i in high:
        same_phase = metrics[i]["ch1"] * metrics[i]["ch2"] >= 0.0
        assignments[i] = "symmetric C-H stretch" if same_phase else "asymmetric C-H stretch"

    if low:
        oop_idx = max(low, key=lambda i: metrics[i]["oop"])
        assignments[oop_idx] = "CH2 out-of-plane bend"
        remaining = [i for i in low if i != oop_idx]
        if remaining:
            co_idx = max(remaining, key=lambda i: abs(metrics[i]["co"]))
            assignments[co_idx] = "C=O stretch"
            remaining.remove(co_idx)
        if remaining:
            scissor_idx = max(remaining, key=lambda i: abs(metrics[i]["angle"]))
            assignments[scissor_idx] = "H-C-H scissoring bend"
            remaining.remove(scissor_idx)
        for i in remaining:
            assignments[i] = "CH2 in-plane rocking bend"

    return [assignment or "mixed vibration" for assignment in assignments]


def make_mode_grids(system, modes: np.ndarray) -> list[Path]:
    grid_paths: list[Path] = []
    optimized_image = FIGURES / "optimized_structure.png"
    view(
        system,
        guess_bonds=len(system.bonds) == 0,
        direction="along_pca3",
        width=500,
        height=400,
        picture_path=str(optimized_image),
    )

    for number, mode in enumerate(modes, start=1):
        max_norm = float(np.linalg.norm(mode, axis=1).max())
        scale = 0.25 / max_norm
        negative = system.copy()
        positive = system.copy()
        negative.coords[:] = np.asarray(system.coords) - scale * mode
        positive.coords[:] = np.asarray(system.coords) + scale * mode

        images = {}
        for label, structure in [
            ("negative displacement", negative),
            ("optimized structure", system),
            ("positive displacement", positive),
        ]:
            image_path = FIGURES / f"mode_{number:02d}_{label.replace(' ', '_')}.png"
            images[label] = view(
                structure,
                guess_bonds=len(structure.bonds) == 0,
                direction="along_pca3",
                width=360,
                height=300,
                picture_path=str(image_path),
            )
        grid_path = FIGURES / f"mode_{number:02d}_grid.png"
        plot_image_grid(images, rows=1, figsize=(12, 3.5), save_path=str(grid_path))
        plt.close("all")
        grid_paths.append(grid_path)
    return grid_paths


def markdown_image(path: Path, alt: str) -> str:
    return f"![{alt}]({path.relative_to(ROOT).as_posix()})"


def format_input(input_text: str) -> str:
    return f"```ams\n{input_text.rstrip()}\n```"


def main() -> None:
    TABLES.mkdir(exist_ok=True)
    FIGURES.mkdir(exist_ok=True)

    job_path = latest_job_path()
    job = AMSJob.load_external(str(job_path))
    provenance = "Combined ADF geometry optimization, harmonic frequencies, IR intensities, and PES-point characterization."

    system = job.results.get_main_system()
    symmetry_probe = system.copy()
    optimized_point_group = symmetry_probe.symmetrize_molecule(tolerance=0.05)
    frequencies = np.asarray(job.results.get_frequencies(unit="cm^-1"), dtype=float)
    intensities = np.asarray(job.results.get_ir_intensities(), dtype=float)
    modes = np.asarray(job.results.get_normal_modes(), dtype=float)
    labels = symmetry_labels(job, len(frequencies))
    assignments = assign_modes(frequencies, modes, system)

    c_idx, o_idx, h_idx = atom_indices(system)
    co_length = system.get_distance(c_idx, o_idx)
    ch_lengths = [system.get_distance(c_idx, h) for h in h_idx]
    hch_angle = system.get_angle(h_idx[0], c_idx, h_idx[1], unit="degree")

    try:
        pes_character = str(job.results.readrkf("AMSResults", "PESPointCharacter", file="engine")).strip()
    except KeyError:
        pes_character = str(job.results.readrkf("AMSResults", "PESPointCharacter", file="ams")).strip()
    imaginary = frequencies[frequencies < 0.0]
    is_minimum = len(imaginary) == 0 and "minimum" in pes_character.lower()

    frequency_table = pd.DataFrame(
        {
            "Mode": np.arange(1, len(frequencies) + 1),
            "Frequency (cm^-1)": np.round(frequencies, 2),
            "IR intensity (km mol^-1)": np.round(intensities, 3),
            "Symmetry": labels,
            "Qualitative assignment": assignments,
        }
    )
    frequency_table.to_csv(TABLES / "vibrational_modes.csv", index=False)

    geometry_table = pd.DataFrame(
        {
            "Quantity": ["C=O", "C-H(1)", "C-H(2)", "mean C-H", "H-C-H"],
            "Value": [co_length, ch_lengths[0], ch_lengths[1], np.mean(ch_lengths), hch_angle],
            "Unit": ["angstrom", "angstrom", "angstrom", "angstrom", "degree"],
        }
    )
    geometry_table.to_csv(TABLES / "optimized_geometry.csv", index=False)

    spectrum_x, spectrum_y = job.results.get_ir_spectrum(
        broadening_type="gaussian",
        broadening_width=20,
        min_x=400,
        max_x=3400,
        x_spacing=1.0,
    )
    fig, ax = plt.subplots(figsize=(8, 4.5))
    ax.plot(spectrum_x, spectrum_y, color="#214f8b", linewidth=1.6)
    ax.vlines(frequencies, 0.0, intensities, color="#a33a2b", linewidth=0.8, alpha=0.7)
    ax.set_xlim(3400, 400)
    ax.set_xlabel(r"Wavenumber (cm$^{-1}$)")
    ax.set_ylabel(r"IR intensity (km mol$^{-1}$)")
    ax.set_title("Harmonic IR spectrum, Gaussian width 20 cm$^{-1}$")
    fig.tight_layout()
    spectrum_path = FIGURES / "ir_spectrum.png"
    fig.savefig(spectrum_path, dpi=180)
    plt.close(fig)

    mode_grid_paths = make_mode_grids(system, modes)
    mode_sections = []
    for row, grid_path in zip(frequency_table.itertuples(index=False), mode_grid_paths):
        mode_sections.append(
            f"### Mode {row.Mode}: {row._1:.2f} cm^-1, {row.Symmetry}\n\n"
            f"Assignment: {row._4}. The left and right structures use equal displacements along the normal coordinate.\n\n"
            f"{markdown_image(grid_path, f'Mode {row.Mode} displacement grid')}"
        )

    if is_minimum:
        conclusion = (
            "The optimized formaldehyde structure is a local minimum. AMS classified the PES point as "
            f"{pes_character}, and all {len(frequencies)} vibrational frequencies are positive."
        )
    else:
        conclusion = (
            "The minimum test did not pass. The PES classification is "
            f"{pes_character}, and the number of negative frequencies is {len(imaginary)}."
        )

    report = f"""# ADF optimization and vibrational analysis of formaldehyde

## Calculation

Formaldehyde was built from the SMILES string `C=O`, symmetrized, and optimized as a neutral closed-shell molecule. The optimized structure has {optimized_point_group} symmetry. ADF used PBE-D3(BJ), the all-electron DZP basis, and default numerical and SCF settings. Harmonic normal modes and IR intensities were calculated at the final optimized geometry.

Job directory: `{job_path.relative_to(ROOT)}`

Provenance: {provenance}

## Optimized geometry

{geometry_table.to_markdown(index=False, floatfmt='.6f')}

{markdown_image(FIGURES / 'optimized_structure.png', 'Optimized formaldehyde structure')}

## Minimum verification

AMS PES-point classification: `{pes_character}`.

Negative harmonic frequencies: {len(imaginary)}.

{conclusion}

## Vibrational modes

The assignments use changes in C=O distance, the two C-H distances, H-C-H angle, and displacement normal to the molecular plane. They are qualitative harmonic-mode descriptions.

{frequency_table.to_markdown(index=False)}

## Broadened IR spectrum

The blue curve applies Gaussian broadening with a width of 20 cm^-1 to the calculated sticks. Red lines mark the unbroadened modes. The wavenumber axis follows the usual IR convention.

{markdown_image(spectrum_path, 'Broadened harmonic IR spectrum')}

## Normal-mode displacement pictures

The maximum atomic displacement in each outer structure is 0.25 angstrom. All views use `along_pca3`.

{chr(10).join(mode_sections)}

## AMS input

{format_input(job.get_input())}

## Conclusion

{conclusion} The optimized bond metrics are C=O {co_length:.6f} angstrom, mean C-H {np.mean(ch_lengths):.6f} angstrom, and H-C-H {hch_angle:.4f} degrees.
"""
    (ROOT / "report.md").write_text(report, encoding="utf-8")
    print(f"Wrote {ROOT / 'report.md'}")
    print(conclusion)


if __name__ == "__main__":
    main()
Original Markdown report
# ADF optimization and vibrational analysis of formaldehyde

## Calculation

Formaldehyde was built from the SMILES string `C=O`, symmetrized, and optimized as a neutral closed-shell molecule. The optimized structure has C(2V) symmetry. ADF used PBE-D3(BJ), the all-electron DZP basis, and default numerical and SCF settings. Harmonic normal modes and IR intensities were calculated at the final optimized geometry.

Job directory: `01-run_workdir/formaldehyde_adf_pbe_dzp`

Provenance: Combined ADF geometry optimization, harmonic frequencies, IR intensities, and PES-point characterization.

## Optimized geometry

| Quantity   |      Value | Unit     |
|:-----------|-----------:|:---------|
| C=O        |   1.212143 | angstrom |
| C-H(1)     |   1.115562 | angstrom |
| C-H(2)     |   1.115562 | angstrom |
| mean C-H   |   1.115562 | angstrom |
| H-C-H      | 115.943895 | degree   |

![Optimized formaldehyde structure](figures/optimized_structure.png)

## Minimum verification

AMS PES-point classification: `local minimum`.

Negative harmonic frequencies: 0.

The optimized formaldehyde structure is a local minimum. AMS classified the PES point as local minimum, and all 6 vibrational frequencies are positive.

## Vibrational modes

The assignments use changes in C=O distance, the two C-H distances, H-C-H angle, and displacement normal to the molecular plane. They are qualitative harmonic-mode descriptions.

|   Mode |   Frequency (cm^-1) |   IR intensity (km mol^-1) | Symmetry   | Qualitative assignment    |
|-------:|--------------------:|---------------------------:|:-----------|:--------------------------|
|      1 |             1157.06 |                      4.682 | B1         | CH2 out-of-plane bend     |
|      2 |             1226.38 |                      8.786 | B2         | CH2 in-plane rocking bend |
|      3 |             1487.73 |                     14.647 | A1         | H-C-H scissoring bend     |
|      4 |             1743.28 |                     88.378 | A1         | C=O stretch               |
|      5 |             2797.14 |                     60.003 | A1         | symmetric C-H stretch     |
|      6 |             2850.66 |                    133.912 | B2         | asymmetric C-H stretch    |

## Broadened IR spectrum

The blue curve applies Gaussian broadening with a width of 20 cm^-1 to the calculated sticks. Red lines mark the unbroadened modes. The wavenumber axis follows the usual IR convention.

![Broadened harmonic IR spectrum](figures/ir_spectrum.png)

## Normal-mode displacement pictures

The maximum atomic displacement in each outer structure is 0.25 angstrom. All views use `along_pca3`.

### Mode 1: 1157.06 cm^-1, B1

Assignment: CH2 out-of-plane bend. The left and right structures use equal displacements along the normal coordinate.

![Mode 1 displacement grid](figures/mode_01_grid.png)
### Mode 2: 1226.38 cm^-1, B2

Assignment: CH2 in-plane rocking bend. The left and right structures use equal displacements along the normal coordinate.

![Mode 2 displacement grid](figures/mode_02_grid.png)
### Mode 3: 1487.73 cm^-1, A1

Assignment: H-C-H scissoring bend. The left and right structures use equal displacements along the normal coordinate.

![Mode 3 displacement grid](figures/mode_03_grid.png)
### Mode 4: 1743.28 cm^-1, A1

Assignment: C=O stretch. The left and right structures use equal displacements along the normal coordinate.

![Mode 4 displacement grid](figures/mode_04_grid.png)
### Mode 5: 2797.14 cm^-1, A1

Assignment: symmetric C-H stretch. The left and right structures use equal displacements along the normal coordinate.

![Mode 5 displacement grid](figures/mode_05_grid.png)
### Mode 6: 2850.66 cm^-1, B2

Assignment: asymmetric C-H stretch. The left and right structures use equal displacements along the normal coordinate.

![Mode 6 displacement grid](figures/mode_06_grid.png)

## AMS input

```ams
Properties
  NormalModes yes
  PESPointCharacter yes
End

Task GeometryOptimization

System
  Atoms
              C       0.0000000000       0.0000000000       0.0337596501
              O       0.0000000000       0.0000000000      -1.1856950809
              H       0.0000000000       0.9391319492       0.5759677154
              H       0.0000000000      -0.9391319492       0.5759677154
  End
  BondOrders
     1 2 2.0
     1 3 1.0
     1 4 1.0
  End
End

Engine adf
  Basis
    Core None
    Type DZP
  End
  XC
    Dispersion GRIMME3 BJDAMP
    GGA PBE
  End
EndEngine
```

## Conclusion

The optimized formaldehyde structure is a local minimum. AMS classified the PES point as local minimum, and all 6 vibrational frequencies are positive. The optimized bond metrics are C=O 1.212143 angstrom, mean C-H 1.115562 angstrom, and H-C-H 115.9439 degrees.