ADF Optimization and Vibrational Analysis of Formaldehyde

Requires: AMS2026 or later

Related documentation
../_images/ir_spectrum_6b6fd8e4.png

Broadened IR spectrum of optimized 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

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.

Note on AI-generated content

This page was generated by a Python script. That Python script was AI-generated.

All numbers, figures, and tables are extracted or postprocessed from actual AMS calculations, and can be transparently regenerated from the provided Python scripts.

Any scientific reasoning or citations was written by AI. This page is the actual one-shot output from using the ams2026 skill with an AI coding agent.