Customized Geometry Optimization Report with an AI Agent

Partially AI-generated content, see the disclaimer.

This example demonstrates how to let an AI agent using the ams2026 skill render a customized Markdown report from a Jinja template. Note: The variables and fields in the template are arbitrarily named, the agent makes sure to populate them with reasonable values. The prompt and the template are shown at the end.

You may want to use this approach to get consistently rendered reports with the data that you need.

../_images/energy_vs_step_3ab0ac59.png

UFF energy during the ethane geometry optimization.

Introduction

Purpose: Optimize a perturbed ethane geometry with UFF and track the energy and carbon-carbon distance through the optimization.

Conclusion: The UFF optimization completed in 57 recorded geometry steps. The energy changed by -16.138 kcal/mol, and the C-C distance changed from 1.5697 to 1.5193 angstrom.

Results

Figures

Energy versus geometry optimization step

Figure 1: UFF energy at each recorded geometry step.

Carbon-carbon distance versus geometry optimization step

Figure 2: Carbon-carbon distance at each recorded geometry step.

Optimized ethane structure

Figure 3: Final ethane geometry from the UFF optimization.

Tables

Table 1: Optimization summary. The complete trajectory is available in ``tables/optimization_history.csv``.

Optimization steps

Initial energy (kcal/mol)

Final energy (kcal/mol)

Energy change (kcal/mol)

Initial C-C distance (angstrom)

Final C-C distance (angstrom)

57

16.280898

0.143193

-16.137705

1.569731

1.519250

Method

Ethane was generated from the SMILES string CC with ChemicalSystem.from_smiles. ChemicalSystem.perturb_coordinates then added a random displacement in the closed interval from -0.1 to +0.1 angstrom to every Cartesian coordinate. AMS optimized this structure with Task GeometryOptimization and the ForceField engine using UFF. Energies and structures were read from the AMS History section. Energy conversion used scm.base.Units.conversion_factor.

Jobs

ethane_uff_optimization

Path: 01-run_workdir/ethane_uff_optimization

Elapsed time: 1.2 s

This job is the sole calculation used in the report. It optimizes a coordinate-perturbed ethane structure with the AMS ForceField engine and the UFF parameter set.

Task GeometryOptimization

System
  Atoms
              C      -0.7981809299      -0.0461488870       0.0949834588
              C       0.7650515814      -0.0312239638      -0.0469214738
              H      -1.2506108108       0.2572869637      -1.0224197006
              H      -1.0532860760       0.7815970450       0.8695181404
              H      -1.0754138638      -1.1597478807       0.4284081238
              H       1.2120688258      -0.2085789922       0.9653654260
              H       1.2103830780      -0.7353841025      -0.7462772478
              H       1.0224876154       1.1027372764      -0.3341921383
  End
  BondOrders
     1 2 1.0
     1 3 1.0
     1 4 1.0
     1 5 1.0
     2 6 1.0
     2 7 1.0
     2 8 1.0
  End
End

Engine ForceField
  Type UFF
EndEngine

Prompts and Python scripts

Prompt (instruction for AI agent)
Use the ams2026 skill.

- Create ethane from smiles `CC`.
- Perturb the coordinates by at most 0.1 angstrom.
- Run a geometry optimization with the UFF force field.

Plot energy vs step and C-C bond length vs step.

Use jinja2 to render the report according to `report.template.md.j2`
report.template.md.j2
# {{ title }}

## Introduction

**Purpose**: {{ purpose }}

**Conclusion**: {{ conclusion }}

## Results

{% if figures %}
### Figures
{% for figure in figures %}
![{{ figure.alt }}]({{ figure.path }})

**Figure {{ loop.index }}**: *{{ figure.caption }}*

{% endfor %}
{% endif %}

{% if tables %}
### Tables

{% for table in tables %}

**Table {{ loop.index }}**: *{{ table.caption }}*

{{ table.df_markdown }}

{% endfor %}
{% endif %}

## Method

{{ method }}

## Jobs

{% for job in jobs %}
### `{{ job.name }}`

**Path**: `{{ job.path }}`

{% if job.elapsed_time %}
**Elapsed time**:  {{ "%.1f" | format(job.elapsed_time) }} s
{% endif %}

{{ job.description }}

```ams
{{ job.input }}
```

{% endfor %}
01-run.py
#!/usr/bin/env amspython
from __future__ import annotations

import numpy as np

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


def main() -> None:
    init(folder="01-run_workdir")

    system = ChemicalSystem.from_smiles("CC")
    coordinates_before = np.asarray(system.coords, dtype=float).copy()
    system.perturb_coordinates(0.1, unit="angstrom")
    maximum_component_change = float(
        np.max(np.abs(np.asarray(system.coords, dtype=float) - coordinates_before))
    )
    if maximum_component_change > 0.1 + 1.0e-12:
        raise RuntimeError("Coordinate perturbation exceeded 0.1 angstrom")

    settings = Settings()
    settings.input.ams.Task = "GeometryOptimization"
    settings.input.ForceField.Type = "UFF"

    job = AMSJob(molecule=system, settings=settings, name="ethane_uff_optimization")
    InputParser().to_dict("ams", job.get_input())
    print(
        "Maximum absolute Cartesian perturbation: "
        "{:.6f} angstrom".format(maximum_component_change)
    )
    job.run()
    finish()


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

import re
from pathlib import Path
from typing import Any, Dict, List

import jinja2
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.ticker import MaxNLocator
from scm.base import Units
from scm.plams import AMSJob, view


WORKDIR_STEM = "01-run_workdir"
JOB_NAME = "ethane_uff_optimization"
TEMPLATE_PATH = Path("report.template.md.j2")


def natural_key(path: Path) -> List[Any]:
    return [
        int(part) if part.isdigit() else part
        for part in re.split(r"(\d+)", str(path))
    ]


def latest_workdir(stem: str) -> Path:
    candidates = [path for path in Path(".").glob("{}*".format(stem)) if path.is_dir()]
    if not candidates:
        raise FileNotFoundError("No PLAMS work directory found for {}".format(stem))
    return max(candidates, key=natural_key)


def carbon_indices(molecule: Any) -> List[int]:
    indices = [
        index
        for index, atom in enumerate(molecule.atoms)
        if atom.symbol == "C"
    ]
    if len(indices) != 2:
        raise ValueError("Expected exactly two carbon atoms, found {}".format(len(indices)))
    return indices


def cc_distance(molecule: Any) -> float:
    first, second = carbon_indices(molecule)
    first_coords = np.asarray(molecule.atoms[first].coords, dtype=float)
    second_coords = np.asarray(molecule.atoms[second].coords, dtype=float)
    return float(np.linalg.norm(first_coords - second_coords))


def render_report() -> None:
    workdir = latest_workdir(WORKDIR_STEM)
    job_path = workdir / JOB_NAME
    job = AMSJob.load_external(str(job_path))
    job_description = (
        "This job is the sole calculation used in the report. It optimizes a "
        "coordinate-perturbed ethane structure with the AMS ForceField engine "
        "and the UFF parameter set."
    )

    history_length = job.results.get_history_length()
    energies_hartree = np.asarray(
        job.results.get_history_property("Energy"), dtype=float
    )
    if len(energies_hartree) != history_length:
        raise ValueError("Energy history length does not match geometry history")

    hartree_to_kcal_mol = Units.conversion_factor("hartree", "kcal/mol")
    energies_kcal_mol = energies_hartree * hartree_to_kcal_mol
    molecules = [
        job.results.get_history_molecule(step)
        for step in range(1, history_length + 1)
    ]
    if any(molecule is None for molecule in molecules):
        raise ValueError("A geometry could not be read from the AMS history")
    cc_lengths = np.asarray([cc_distance(molecule) for molecule in molecules])
    steps = np.arange(1, history_length + 1)

    tables_dir = Path("tables")
    figures_dir = Path("figures")
    tables_dir.mkdir(exist_ok=True)
    figures_dir.mkdir(exist_ok=True)

    trajectory = pd.DataFrame(
        {
            "Step": steps,
            "Energy (kcal/mol)": energies_kcal_mol,
            "C-C distance (angstrom)": cc_lengths,
        }
    )
    trajectory.to_csv(tables_dir / "optimization_history.csv", index=False)

    summary = pd.DataFrame(
        [
            {
                "Optimization steps": history_length,
                "Initial energy (kcal/mol)": energies_kcal_mol[0],
                "Final energy (kcal/mol)": energies_kcal_mol[-1],
                "Energy change (kcal/mol)": energies_kcal_mol[-1]
                - energies_kcal_mol[0],
                "Initial C-C distance (angstrom)": cc_lengths[0],
                "Final C-C distance (angstrom)": cc_lengths[-1],
            }
        ]
    )
    summary.to_csv(tables_dir / "optimization_summary.csv", index=False)

    fig, ax = plt.subplots(figsize=(6.4, 4.0))
    ax.plot(steps, energies_kcal_mol, marker="o", linewidth=1.5)
    ax.set_xlabel("Optimization step")
    ax.set_ylabel("Energy (kcal/mol)")
    ax.xaxis.set_major_locator(MaxNLocator(nbins=9, integer=True))
    ax.grid(alpha=0.25)
    fig.tight_layout()
    energy_path = figures_dir / "energy_vs_step.png"
    fig.savefig(str(energy_path), dpi=180)
    plt.close(fig)

    fig, ax = plt.subplots(figsize=(6.4, 4.0))
    ax.plot(steps, cc_lengths, marker="o", linewidth=1.5)
    ax.set_xlabel("Optimization step")
    ax.set_ylabel("C-C distance (angstrom)")
    ax.xaxis.set_major_locator(MaxNLocator(nbins=9, integer=True))
    ax.grid(alpha=0.25)
    fig.tight_layout()
    distance_path = figures_dir / "cc_distance_vs_step.png"
    fig.savefig(str(distance_path), dpi=180)
    plt.close(fig)

    optimized_system = job.results.get_main_system()
    structure_path = figures_dir / "optimized_ethane.png"
    view(
        optimized_system,
        guess_bonds=len(optimized_system.bonds) == 0,
        direction="along_pca3",
        width=600,
        height=450,
        picture_path=str(structure_path),
    )

    timings = job.results.get_timings()
    elapsed_time = float(timings.get("elapsed", timings.get("total", 0.0)))
    energy_change = energies_kcal_mol[-1] - energies_kcal_mol[0]
    conclusion = (
        "The UFF optimization completed in {} recorded geometry steps. The "
        "energy changed by {:.3f} kcal/mol, and the C-C distance changed from "
        "{:.4f} to {:.4f} angstrom."
    ).format(history_length, energy_change, cc_lengths[0], cc_lengths[-1])
    method = (
        "Ethane was generated from the SMILES string `CC` with "
        "`ChemicalSystem.from_smiles`. `ChemicalSystem.perturb_coordinates` "
        "then added a random displacement in the closed interval from -0.1 to "
        "+0.1 angstrom to every Cartesian coordinate. AMS optimized this "
        "structure with `Task GeometryOptimization` and the ForceField engine "
        "using UFF. Energies and structures were read from the AMS History "
        "section. Energy conversion used `scm.base.Units.conversion_factor`."
    )

    context: Dict[str, Any] = {
        "title": "UFF geometry optimization of ethane",
        "purpose": (
            "Optimize a perturbed ethane geometry with UFF and track the "
            "energy and carbon-carbon distance through the optimization."
        ),
        "conclusion": conclusion,
        "figures": [
            {
                "alt": "Energy versus geometry optimization step",
                "path": str(energy_path),
                "caption": "UFF energy at each recorded geometry step.",
            },
            {
                "alt": "Carbon-carbon distance versus geometry optimization step",
                "path": str(distance_path),
                "caption": "Carbon-carbon distance at each recorded geometry step.",
            },
            {
                "alt": "Optimized ethane structure",
                "path": str(structure_path),
                "caption": "Final ethane geometry from the UFF optimization.",
            },
        ],
        "tables": [
            {
                "caption": (
                    "Optimization summary. The complete trajectory is available "
                    "in `tables/optimization_history.csv`."
                ),
                "df_markdown": summary.to_markdown(
                    index=False,
                    floatfmt=(".0f", ".6f", ".6f", ".6f", ".6f", ".6f"),
                ),
            }
        ],
        "method": method,
        "jobs": [
            {
                "name": job.name,
                "path": str(job_path),
                "elapsed_time": elapsed_time,
                "description": job_description,
                "input": job.get_input().strip(),
            }
        ],
    }

    template = jinja2.Environment(
        loader=jinja2.FileSystemLoader(str(TEMPLATE_PATH.parent)),
        autoescape=False,
        keep_trailing_newline=True,
    ).get_template(TEMPLATE_PATH.name)
    Path("report.md").write_text(template.render(**context), encoding="utf-8")


if __name__ == "__main__":
    render_report()
Original Markdown report
# UFF geometry optimization of ethane

## Introduction

**Purpose**: Optimize a perturbed ethane geometry with UFF and track the energy and carbon-carbon distance through the optimization.

**Conclusion**: The UFF optimization completed in 57 recorded geometry steps. The energy changed by -16.138 kcal/mol, and the C-C distance changed from 1.5697 to 1.5193 angstrom.

## Results


### Figures

![Energy versus geometry optimization step](figures/energy_vs_step.png)

**Figure 1**: *UFF energy at each recorded geometry step.*


![Carbon-carbon distance versus geometry optimization step](figures/cc_distance_vs_step.png)

**Figure 2**: *Carbon-carbon distance at each recorded geometry step.*


![Optimized ethane structure](figures/optimized_ethane.png)

**Figure 3**: *Final ethane geometry from the UFF optimization.*





### Tables



**Table 1**: *Optimization summary. The complete trajectory is available in `tables/optimization_history.csv`.*

|   Optimization steps |   Initial energy (kcal/mol) |   Final energy (kcal/mol) |   Energy change (kcal/mol) |   Initial C-C distance (angstrom) |   Final C-C distance (angstrom) |
|---------------------:|----------------------------:|--------------------------:|---------------------------:|----------------------------------:|--------------------------------:|
|                   57 |                   16.280898 |                  0.143193 |                 -16.137705 |                          1.569731 |                        1.519250 |




## Method

Ethane was generated from the SMILES string `CC` with `ChemicalSystem.from_smiles`. `ChemicalSystem.perturb_coordinates` then added a random displacement in the closed interval from -0.1 to +0.1 angstrom to every Cartesian coordinate. AMS optimized this structure with `Task GeometryOptimization` and the ForceField engine using UFF. Energies and structures were read from the AMS History section. Energy conversion used `scm.base.Units.conversion_factor`.

## Jobs


### `ethane_uff_optimization`

**Path**: `01-run_workdir/ethane_uff_optimization`


**Elapsed time**:  1.2 s


This job is the sole calculation used in the report. It optimizes a coordinate-perturbed ethane structure with the AMS ForceField engine and the UFF parameter set.

```ams
Task GeometryOptimization

System
  Atoms
              C      -0.7981809299      -0.0461488870       0.0949834588
              C       0.7650515814      -0.0312239638      -0.0469214738
              H      -1.2506108108       0.2572869637      -1.0224197006
              H      -1.0532860760       0.7815970450       0.8695181404
              H      -1.0754138638      -1.1597478807       0.4284081238
              H       1.2120688258      -0.2085789922       0.9653654260
              H       1.2103830780      -0.7353841025      -0.7462772478
              H       1.0224876154       1.1027372764      -0.3341921383
  End
  BondOrders
     1 2 1.0
     1 3 1.0
     1 4 1.0
     1 5 1.0
     2 6 1.0
     2 7 1.0
     2 8 1.0
  End
End

Engine ForceField
  Type UFF
EndEngine
```

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.