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.
Requires: AMS2026 or later
Related documentation
Related examples
Related tutorials
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¶
Figure 1: UFF energy at each recorded geometry step.
Figure 2: Carbon-carbon distance at each recorded geometry step.
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 {{ 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

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

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

**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.