Python bindings#

Python bindings are provided through a ctypes-based interface. The shared library is bundled into a binary wheel, so no Fortran or C compiler is needed at install time.

Installation#

Binary wheels for Linux (x86_64, aarch64) and macOS (arm64) are published on PyPI:

pip install gfnff          # library only
pip install "gfnff[ase]"   # + ASE (enables the CLI and the ASE calculator)

Building from source#

The source build compiles the Fortran library on your machine. The following system packages must be present before running pip:

Dependency

Example (Debian/Ubuntu)

Example (Fedora/RHEL)

Example (macOS)

Fortran compiler

apt install gfortran

dnf install gcc-gfortran

brew install gcc

LAPACK + BLAS

apt install libopenblas-dev

dnf install openblas-devel

brew install openblas

CMake ≥ 3.21

installed by pip automatically

←

←

Once those are in place:

pip install ".[ase]"           # from a checkout
pip install "gfnff[ase]" --no-binary gfnff   # force source build from PyPI

Command-line interface#

Installing gfnff[ase] places a gfnff executable on your PATH.

gfnff <input> [options]

The input file is read by ASE, so any format it supports works (xyz, extxyz, POSCAR, cif, …).

Singlepoint (default):

gfnff molecule.xyz
gfnff molecule.xyz --chrg -1
gfnff molecule.xyz --alpb h2o        # implicit solvation (--solv is an alias)

Geometry optimisation (L-BFGS via ASE, cell fixed, writes gfnff.log.extxyz):

gfnff molecule.xyz --opt
gfnff molecule.xyz --opt --fmax 0.05          # looser convergence, eV/Å
gfnff molecule.xyz --opt --outfile path.xyz   # custom trajectory file
gfnff molecule.xyz --opt --alpb acetone       # optimise in solvent

Variable-cell optimisation (L-BFGS + ExpCellFilter, periodic systems only):

gfnff crystal.cif --optcell
gfnff crystal.cif --optcell --fmax 0.01

Full option list: gfnff --help

The trajectory file (gfnff.log.extxyz) stores energy and forces in each frame header, compatible with ASE’s ase gui.

Low-level API (GFNFFCalculator)#

GFNFFCalculator mirrors the C API one-to-one. All quantities use the same units as the library itself: Bohr for coordinates and lattice, Hartree for energy, Eh/Bohr for gradients, and Hartree for the stress tensor.

import numpy as np
from gfnff import GFNFFCalculator

# Atomic numbers and coordinates in Bohr
numbers = np.array([6, 8, 1, 1], dtype=np.int32)   # CO + 2 H
positions = np.array([[0, 0, 0], [2.1, 0, 0],
                      [-1.0, 0, 0], [3.1, 0, 0]], dtype=np.float64)

with GFNFFCalculator(numbers, positions, charge=0, printlevel=0) as calc:
    energy, gradient, sigma = calc.singlepoint(numbers, positions)
    print(f"Energy: {energy:.6f} Eh")
    print(f"Gradient shape: {gradient.shape}")  # (nat, 3)
    print(f"Stress tensor:\n{sigma}")            # (3, 3), Hartree; zero for non-PBC

Periodic systems use a separate initialiser:

calc = GFNFFCalculator(
    numbers, positions,
    lattice=lattice_bohr,   # shape (3, 3), rows are lattice vectors
    npbc=3,
)

Partial charges are those of the last singlepoint, where they are obtained as part of the energy evaluation:

with GFNFFCalculator(numbers, positions) as calc:
    calc.singlepoint(numbers, positions)
    q = calc.charges()      # (nat,), in e; sums to the total charge

Force-field versions, custom parameter files and user-supplied molecular graphs are described in parametrisation.md.

ASE Calculator (GFNFF)#

GFNFF is a fully compatible ASE Calculator. It handles unit conversion automatically (Å ↔ Bohr, eV ↔ Hartree). Implemented properties: energy, forces, stress, charges.

from ase.build import molecule
from gfnff import GFNFF

atoms = molecule("caffeine")
atoms.calc = GFNFF()

energy = atoms.get_potential_energy()   # eV
forces = atoms.get_forces()             # eV / Å, shape (nat, 3)
stress = atoms.get_stress()             # eV / ų, Voigt [xx,yy,zz,yz,xz,xy]; zero for non-PBC
charges = atoms.get_charges()           # e, shape (nat,)

Periodic systems work the same way; provide an atoms object with cell and pbc set:

from ase.io import read
from gfnff import GFNFF

atoms = read("quartz.cif")
atoms.calc = GFNFF()
print(atoms.get_potential_energy())  # eV / unit cell
print(atoms.get_stress())            # eV / ų, Voigt

Variable-cell relaxation via ASE’s ExpCellFilter:

from ase.filters import ExpCellFilter
from ase.optimize import LBFGS

opt = LBFGS(ExpCellFilter(atoms))
opt.run(fmax=0.01)

Hessian and vibrations#

The Hessian is an explicit method rather than one of implemented_properties, so it is never computed as a side effect of an MD or optimisation step:

hessian = atoms.calc.get_hessian(atoms)   # (3N, 3N), eV / Ų

vib = atoms.calc.get_vibrations(atoms)    # ase.vibrations.VibrationsData
vib.get_energies()                        # frequencies, normal modes,
vib.get_modes()                           # thermochemistry, all from ASE

get_vibrations() passes the analytic Hessian directly to ASE, which avoids the 6N displaced singlepoints that ase.vibrations.Vibrations would otherwise run. Results for an unchanged geometry are cached, so asking for frequencies and modes does not evaluate it twice. Non-periodic systems only.

Additional options:

Parameter

Default

Description

charge

0

Total charge. Also reads atoms.info["charge"] (takes precedence).

solvent

""

Implicit solvent name: "h2o", "acetone", "chcl3", … (molecular systems only)

printlevel

0

Fortran output verbosity (0 = silent, 3 = verbose).

version

None

Force-field version; see parametrisation.md.

parametrisation

None

Path to a parameter file, overlaid on that version.

Changing any of these with calc.set(...) rebuilds the underlying force field rather than reusing the cached result.

Running the tests#

pip install "gfnff[test]"
pytest python/tests/