Customized 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_f83be765.png

UFF energy during the ethane geometry optimization.

Introduction

This example shows how the energy and C-C bond length for ethane (SMILES CC) varies as a function of frame when using the default AMS geometry optimizer with the UFF force field.

Method

Ethane was created from smiles CC, and the coordinates were perturbed randomly by up to 0.05 angstrom.

Results

Energy versus step

../_images/energy_vs_step_f83be765.png

Figure 1: Energy in eV vs. step number.

C-C bond length versus step

../_images/c_c_bond_length_vs_step_8f590bc2.png

Figure 2: C-C bond length in angstrom vs. step number.

Additional Figures

../_images/optimized_ethane_0106d999.png

Figure 3: Final optimized ethane structure

Tables

Table 1: UFF geometry-optimization history. Data in tables/optimization_history.csv.

Step

Energy (eV)

C-C bond length (angstrom)

1

0.349953

1.507598

2

0.336099

1.508151

3

0.296968

1.509783

4

0.239415

1.512406

5

0.173525

1.515877

6

0.110833

1.519987

7

0.062249

1.524456

8

0.034775

1.529320

9

0.038871

1.533157

10

0.038595

1.533076

11

0.037784

1.532836

12

0.036486

1.532441

13

0.034780

1.531900

14

0.032766

1.531223

15

0.030559

1.530426

16

0.028054

1.529428

17

0.025389

1.528217

18

0.022779

1.526794

19

0.020490

1.525198

20

0.018751

1.523551

21

0.017611

1.522058

22

0.017577

1.522075

23

0.017479

1.522126

24

0.017317

1.522209

25

0.017099

1.522324

26

0.016829

1.522467

27

0.016515

1.522635

28

0.016129

1.522844

29

0.015668

1.523094

30

0.015134

1.523383

31

0.014533

1.523699

32

0.013873

1.524016

33

0.013164

1.524290

34

0.012406

1.524458

35

0.011594

1.524451

36

0.010720

1.524208

37

0.009790

1.523685

38

0.008856

1.522870

39

0.008030

1.521807

40

0.007457

1.520670

41

0.007187

1.519816

42

0.007111

1.519386

43

0.007114

1.519349

44

0.007210

1.519469

45

0.007191

1.519446

46

0.007138

1.519383

47

0.007053

1.519285

48

0.006946

1.519167

49

0.006825

1.519046

50

0.006697

1.518941

51

0.006558

1.518866

52

0.006418

1.518854

53

0.006292

1.518934

54

0.006201

1.519112

Conclusion

The optimization contains 54 stored steps. The final UFF energy is 0.0062 eV and the final C-C bond length is 1.519 angstrom.

AMS Jobs

ethane_uff_optimization

Path: 01-run_workdir/ethane_uff_optimization

Elapsed time: 0.6 s

UFF geometry optimization of randomly perturbed ethane. This job provides the complete optimization history and final structure.

Task GeometryOptimization

System
  Atoms
              C      -0.7125144885      -0.0353733512       0.0782009170
              C       0.7904491320       0.0161435287      -0.0280875554
              H      -1.2208114243       0.1782613175      -0.9910626077
              H      -1.1613170436       0.6517431022       0.7610152982
              H      -1.0485308381      -1.0341335001       0.3365061019
              H       1.1791437088      -0.1392159691       0.9734877502
              H       1.1595506739      -0.6672605251      -0.8075175853
              H       1.1043735263       1.1102780791      -0.3555186743
  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 scripts that run the required jobs and that can
render the jinja2 template in `report.template.md.j2` by passing the required
context.
report.template.md.j2
# {{ title }}

## Introduction

This example shows how the energy and C-C bond length for **ethane** (SMILES
`CC`) varies as a function of frame when using the default AMS geometry
optimizer with the UFF force field.

## Method

Ethane was created from smiles `CC`, and the coordinates were perturbed
randomly by up to 0.05 angstrom.

{% set NUMFIGS = 0 %}

## Results

### Energy versus step

![]({{ fig_e_vs_step.path }})

{% set NUMFIGS = NUMFIGS + 1 %}
**Figure {{ NUMFIGS }}**: *Energy in eV vs. step number*.

### C-C bond length versus step

![]({{ fig_c_c_bond_length_vs_step.path }})

{% set NUMFIGS = NUMFIGS + 1 %}
**Figure {{ NUMFIGS }}**: *C-C bond length in angstrom vs. step number*.

{% if extra_figures %}
### Additional Figures
{% for fig in extra_figures %}
![]({{ fig.path }})

{% set NUMFIGS = NUMFIGS + 1 %}
**Figure {{ NUMFIGS }}**: *{{ fig.caption }}*

{% endfor %}
{% endif %}

{% if extra_tables %}
### Tables

{% for table in extra_tables %}

**Table {{ loop.index }}**: *{{ table.caption }}*. Data in `{{ table.path }}`.

{{ table.df_markdown }}

{% endfor %}
{% endif %}

## Conclusion

The optimization contains {{ num_steps }} stored steps. The final UFF energy is
{{ "%.4f" | format(final_energy) }} eV and the final C-C bond length is
{{ "%.3f" | format(final_bond_length) }} angstrom.

## AMS Jobs

{% for job_info in job_infos %}
### `{{ job_info.name }}`

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

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

{{ job_info.description }}

```ams
{{ job_info.input }}
```

{% endfor %}
01-run.py
#!/usr/bin/env amspython
"""Run the UFF geometry optimization used by the report."""

from __future__ import annotations

import os
from pathlib import Path

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


WORKDIR = "01-run_workdir"
JOB_NAME = "ethane_uff_optimization"


def build_system() -> ChemicalSystem:
    """Build ethane and apply the requested random coordinate displacement."""
    system = ChemicalSystem.from_smiles("CC")
    system.perturb_coordinates(0.05, unit="angstrom")
    return system


def build_settings() -> Settings:
    """Return the default AMS geometry optimizer with the UFF engine."""
    settings = Settings()
    settings.input.ams.Task = "GeometryOptimization"
    settings.input.ForceField.Type = "UFF"
    return settings


def main() -> None:
    os.chdir(Path(__file__).resolve().parent)
    init(folder=WORKDIR)
    try:
        job = AMSJob(
            name=JOB_NAME,
            molecule=build_system(),
            settings=build_settings(),
        )
        InputParser().to_dict("ams", job.get_input())
        print(job.get_input())
        job.run()
    finally:
        finish()


if __name__ == "__main__":
    main()
report.py
#!/usr/bin/env amspython
"""Read the latest finished AMS job and render report.template.md.j2."""

from __future__ import annotations

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

import matplotlib.pyplot as plt
import pandas as pd
from jinja2 import Environment, FileSystemLoader, StrictUndefined
from scm.base import Units
from scm.plams import AMSJob, Molecule, view


ROOT = Path(__file__).resolve().parent
WORKDIR_STEM = "01-run_workdir"
JOB_NAME = "ethane_uff_optimization"


def natural_key(path: Path) -> List[Any]:
    """Return a key that sorts numeric suffixes by numeric value."""
    return [
        int(part) if part.isdigit() else part
        for part in re.split(r"(\d+)", path.name)
    ]


def latest_workdir(stem: str) -> Path:
    """Find the newest PLAMS work directory by its natural-sort suffix."""
    candidates = [path for path in ROOT.glob("{}*".format(stem)) if path.is_dir()]
    if not candidates:
        raise FileNotFoundError(
            "No {} directory found. Run 01-run.py first.".format(stem)
        )
    return max(candidates, key=natural_key)


def carbon_bond_length(molecule: Molecule) -> float:
    """Return the distance between the two carbon atoms in angstrom."""
    carbon_atoms = [atom for atom in molecule.atoms if atom.symbol == "C"]
    if len(carbon_atoms) != 2:
        raise ValueError("Expected exactly two carbon atoms in ethane")
    return float(carbon_atoms[0].distance_to(carbon_atoms[1]))


def save_line_plot(
    steps: List[int],
    values: List[float],
    ylabel: str,
    output_path: Path,
) -> None:
    """Write one optimization-history line plot."""
    fig, ax = plt.subplots(figsize=(6.4, 4.0))
    ax.plot(steps, values, marker="o")
    ax.set_xlabel("Optimization step")
    ax.set_ylabel(ylabel)
    ax.grid(alpha=0.25)
    fig.tight_layout()
    fig.savefig(str(output_path), dpi=160)
    plt.close(fig)


def main() -> None:
    workdir = latest_workdir(WORKDIR_STEM)
    job_path = workdir / JOB_NAME

    # This job supplies every numerical result and structure cited in the report.
    job = AMSJob.load_external(str(job_path))

    num_steps = job.results.get_history_length()
    raw_energies = job.results.get_history_property("Energy")
    if raw_energies is None or len(raw_energies) != num_steps:
        raise ValueError("The AMS history does not contain one energy per step")

    hartree_to_ev = Units.conversion_factor("hartree", "eV")
    energies = [float(value) * hartree_to_ev for value in raw_energies]
    molecules = [job.results.get_history_molecule(step) for step in range(1, num_steps + 1)]
    if any(molecule is None for molecule in molecules):
        raise ValueError("The AMS history is missing one or more molecular frames")
    bond_lengths = [carbon_bond_length(molecule) for molecule in molecules]
    steps = list(range(1, num_steps + 1))

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

    energy_figure = figures_dir / "energy_vs_step.png"
    bond_figure = figures_dir / "c_c_bond_length_vs_step.png"
    structure_figure = figures_dir / "optimized_ethane.png"
    save_line_plot(steps, energies, "Energy (eV)", energy_figure)
    save_line_plot(steps, bond_lengths, "C-C bond length (angstrom)", bond_figure)

    final_system = job.results.get_main_system()
    view(
        final_system,
        guess_bonds=len(final_system.bonds) == 0,
        direction="along_pca3",
        width=600,
        height=450,
        picture_path=str(structure_figure),
    )

    history = pd.DataFrame(
        {
            "Step": steps,
            "Energy (eV)": energies,
            "C-C bond length (angstrom)": bond_lengths,
        }
    )
    table_path = tables_dir / "optimization_history.csv"
    history.to_csv(table_path, index=False)

    elapsed_time = job.results.get_timings().get("elapsed")
    context: Dict[str, Any] = {
        "title": "UFF geometry optimization of ethane",
        "fig_e_vs_step": {"path": energy_figure.relative_to(ROOT).as_posix()},
        "fig_c_c_bond_length_vs_step": {
            "path": bond_figure.relative_to(ROOT).as_posix()
        },
        "extra_figures": [
            {
                "path": structure_figure.relative_to(ROOT).as_posix(),
                "caption": "Final optimized ethane structure",
            }
        ],
        "extra_tables": [
            {
                "path": table_path.relative_to(ROOT).as_posix(),
                "caption": "UFF geometry-optimization history",
                "df_markdown": history.to_markdown(
                    index=False, floatfmt=(".0f", ".6f", ".6f")
                ),
            }
        ],
        "num_steps": num_steps,
        "final_energy": energies[-1],
        "final_bond_length": bond_lengths[-1],
        "job_infos": [
            {
                "name": JOB_NAME,
                "path": job_path.relative_to(ROOT).as_posix(),
                "elapsed_time": elapsed_time,
                "description": (
                    "UFF geometry optimization of randomly perturbed ethane. "
                    "This job provides the complete optimization history and final structure."
                ),
                "input": job.get_input().rstrip(),
            }
        ],
    }

    environment = Environment(
        loader=FileSystemLoader(str(ROOT)),
        undefined=StrictUndefined,
        autoescape=False,
        keep_trailing_newline=True,
    )
    report = environment.get_template("report.template.md.j2").render(**context)
    (ROOT / "report.md").write_text(report, encoding="utf-8")


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

## Introduction

This example shows how the energy and C-C bond length for **ethane** (SMILES
`CC`) varies as a function of frame when using the default AMS geometry
optimizer with the UFF force field.

## Method

Ethane was created from smiles `CC`, and the coordinates were perturbed
randomly by up to 0.05 angstrom.



## Results

### Energy versus step

![](figures/energy_vs_step.png)


**Figure 1**: *Energy in eV vs. step number*.

### C-C bond length versus step

![](figures/c_c_bond_length_vs_step.png)


**Figure 2**: *C-C bond length in angstrom vs. step number*.


### Additional Figures

![](figures/optimized_ethane.png)


**Figure 3**: *Final optimized ethane structure*





### Tables



**Table 1**: *UFF geometry-optimization history*. Data in `tables/optimization_history.csv`.

|   Step |   Energy (eV) |   C-C bond length (angstrom) |
|-------:|--------------:|-----------------------------:|
|      1 |      0.349953 |                     1.507598 |
|      2 |      0.336099 |                     1.508151 |
|      3 |      0.296968 |                     1.509783 |
|      4 |      0.239415 |                     1.512406 |
|      5 |      0.173525 |                     1.515877 |
|      6 |      0.110833 |                     1.519987 |
|      7 |      0.062249 |                     1.524456 |
|      8 |      0.034775 |                     1.529320 |
|      9 |      0.038871 |                     1.533157 |
|     10 |      0.038595 |                     1.533076 |
|     11 |      0.037784 |                     1.532836 |
|     12 |      0.036486 |                     1.532441 |
|     13 |      0.034780 |                     1.531900 |
|     14 |      0.032766 |                     1.531223 |
|     15 |      0.030559 |                     1.530426 |
|     16 |      0.028054 |                     1.529428 |
|     17 |      0.025389 |                     1.528217 |
|     18 |      0.022779 |                     1.526794 |
|     19 |      0.020490 |                     1.525198 |
|     20 |      0.018751 |                     1.523551 |
|     21 |      0.017611 |                     1.522058 |
|     22 |      0.017577 |                     1.522075 |
|     23 |      0.017479 |                     1.522126 |
|     24 |      0.017317 |                     1.522209 |
|     25 |      0.017099 |                     1.522324 |
|     26 |      0.016829 |                     1.522467 |
|     27 |      0.016515 |                     1.522635 |
|     28 |      0.016129 |                     1.522844 |
|     29 |      0.015668 |                     1.523094 |
|     30 |      0.015134 |                     1.523383 |
|     31 |      0.014533 |                     1.523699 |
|     32 |      0.013873 |                     1.524016 |
|     33 |      0.013164 |                     1.524290 |
|     34 |      0.012406 |                     1.524458 |
|     35 |      0.011594 |                     1.524451 |
|     36 |      0.010720 |                     1.524208 |
|     37 |      0.009790 |                     1.523685 |
|     38 |      0.008856 |                     1.522870 |
|     39 |      0.008030 |                     1.521807 |
|     40 |      0.007457 |                     1.520670 |
|     41 |      0.007187 |                     1.519816 |
|     42 |      0.007111 |                     1.519386 |
|     43 |      0.007114 |                     1.519349 |
|     44 |      0.007210 |                     1.519469 |
|     45 |      0.007191 |                     1.519446 |
|     46 |      0.007138 |                     1.519383 |
|     47 |      0.007053 |                     1.519285 |
|     48 |      0.006946 |                     1.519167 |
|     49 |      0.006825 |                     1.519046 |
|     50 |      0.006697 |                     1.518941 |
|     51 |      0.006558 |                     1.518866 |
|     52 |      0.006418 |                     1.518854 |
|     53 |      0.006292 |                     1.518934 |
|     54 |      0.006201 |                     1.519112 |




## Conclusion

The optimization contains 54 stored steps. The final UFF energy is
0.0062 eV and the final C-C bond length is
1.519 angstrom.

## AMS Jobs


### `ethane_uff_optimization`

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


**Elapsed time**:  0.6 s


UFF geometry optimization of randomly perturbed ethane. This job provides the complete optimization history and final structure.

```ams
Task GeometryOptimization

System
  Atoms
              C      -0.7125144885      -0.0353733512       0.0782009170
              C       0.7904491320       0.0161435287      -0.0280875554
              H      -1.2208114243       0.1782613175      -0.9910626077
              H      -1.1613170436       0.6517431022       0.7610152982
              H      -1.0485308381      -1.0341335001       0.3365061019
              H       1.1791437088      -0.1392159691       0.9734877502
              H       1.1595506739      -0.6672605251      -0.8075175853
              H       1.1043735263       1.1102780791      -0.3555186743
  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.