Torsiondrive calculations#

Torsiondrives perform constrained scans over one or more dihedral angles. At each grid point (a specific set of dihedral values), a constrained geometry optimization is performed to minimize the energy while keeping the target dihedral(s) fixed. This produces a map of optimized structures and energies across the dihedral grid.

Torsiondrive Records#

Torsiondrive records contain all the fields of a base record, and additionally include:

  • initial_molecules - The starting molecules for the scan (one torsiondrive can start from multiple initial geometries)

  • optimizations - A mapping from grid keys to lists of constrained OptimizationRecord objects

  • minimum_optimizations - A mapping from grid keys to the selected lowest-energy OptimizationRecord for each grid point

  • specification - The scan/optimization setup (see below)

Grid keys identify points in the dihedral scan. They are tuples whose length equals the number of scanned dihedrals.

Convenience properties include:

  • optimizations - Returns a dictionary mapping grid keys (tuples) to lists of optimization records run at that key.

  • minimum_optimizations - Returns a dictionary mapping grid keys (tuples) to the lowest-energy optimization record at each key.

  • final_energies - Returns a dictionary mapping grid keys (tuples) to the final energy (a.u.) from the minimum optimization at that key.

Torsiondrive Specification#

The specification for a torsiondrive is a TorsiondriveSpecification. The key fields are:

  • program - The torsiondrive driver program ("torsiondrive")

  • optimization_specification - How each constrained optimization should be run; see Optimization Specification

  • keywords - Torsiondrive scan definition and related options

Keywords are provided via TorsiondriveKeywords and include:

  • dihedrals (list of 4-tuples of ints) - The dihedral indices to scan.

  • grid_spacing (list of ints, degrees) - The grid spacing for each dihedral. If multiple values are given, they are paired element-wise with the dihedrals.

  • dihedral_ranges (optional list of (lower, upper) degree pairs) - Limits the scan range for each dihedral.

  • energy_decrease_thresh (float, optional) - Smallest energy decrease to trigger launching new optimizations from a grid point.

  • energy_upper_limit (float, optional, a.u.) - Skip launching new optimizations whose starting energy is above the current global minimum by more than this amount.

Note: The constrained optimizations at each grid point use the details supplied by the OptimizationSpecification, which in turn references the singlepoint QC settings inside its qc_specification. See Singlepoint Specification (QCSpecification) and Optimization Specification for more.

Important

Every field of TorsiondriveKeywords has a default, so TorsiondriveKeywords() is valid and describes no scan at all. That is deliberate: in a torsiondrive dataset the keywords are normally left empty in the specification and supplied per entry instead, because dihedrals refers to atom indices that differ from molecule to molecule. See How entry and specification keywords are combined.

When submitting a single record with add_torsiondrives() there are no entries, so the keywords must be given here.

Basic TorsiondriveSpecification with a single dihedral
from qcportal.torsiondrive import TorsiondriveSpecification, TorsiondriveKeywords
from qcportal.optimization import OptimizationSpecification
from qcportal.singlepoint import QCSpecification

td_spec = TorsiondriveSpecification(
    program="torsiondrive",
    optimization_specification=OptimizationSpecification(
        program="geometric",
        qc_specification=QCSpecification(program="psi4", method="b3lyp", basis="def2-svp", driver="deferred"),
    ),
    keywords=TorsiondriveKeywords(
        dihedrals=[(0, 1, 2, 3)],
        grid_spacing=[15],
    ),
)
Multiple dihedrals with different spacings and ranges
from qcportal.torsiondrive import TorsiondriveSpecification, TorsiondriveKeywords
from qcportal.optimization import OptimizationSpecification
from qcportal.singlepoint import QCSpecification

td_spec = TorsiondriveSpecification(
    program="torsiondrive",
    optimization_specification=OptimizationSpecification(
        program="geometric",
        qc_specification=QCSpecification(program="psi4", method="wb97x-d", basis="def2-tzvp", driver="deferred"),
    ),
    keywords=TorsiondriveKeywords(
        dihedrals=[(0, 1, 2, 3), (1, 2, 3, 4)],
        grid_spacing=[15, 30],
        dihedral_ranges=[(-180, 180), (-120, 120)],
        energy_upper_limit=0.05,
    ),
)

Submitting Records#

Torsiondrive records can be submitted using a client via the add_torsiondrives() method. This method takes the following key information:

  • initial_molecules - A list of lists of molecules. Each torsiondrive may start from multiple initial molecules, so pass a nested list (one inner list per record).

  • program - The torsiondrive program (use "torsiondrive")

  • optimization_specification - The optimization setup for each grid point (see Torsiondrive Specification)

  • keywords - The torsiondrive dihedral scan definition and options

See Submitting computations for more information about other common fields such as compute tag and priority.

Client Examples#

Obtain a single torsiondrive record by ID
r = client.get_torsiondrives(123)
Obtain multiple torsiondrive records by ID
r_lst = client.get_torsiondrives([123, 456])
Include child optimizations and their data during fetch
r = client.get_torsiondrives(123, include=["optimizations", "**"])  # include full child data
# Access optimizations via a mapping from grid keys (tuples) to lists of OptimizationRecord
for key, opt_list in r.optimizations.items():
    print(key, [opt.id for opt in opt_list])
Query torsiondrives by QC method/basis and include optimizations
r_iter = client.query_torsiondrives(qc_method='b3lyp', qc_basis='def2-svp', include=['optimizations'])
for r in r_iter:
    print(r.id, len(r.optimizations))
Submit torsiondrives with two different starting geometries
from qcportal.torsiondrive import TorsiondriveSpecification, TorsiondriveKeywords
from qcportal.optimization import OptimizationSpecification
from qcportal.singlepoint import QCSpecification

td_spec = TorsiondriveSpecification(
    program='torsiondrive',
    optimization_specification=OptimizationSpecification(
        program='geometric',
        qc_specification=QCSpecification(program='psi4', method='b3lyp', basis='def2-svp', driver='deferred'),
    ),
    keywords=TorsiondriveKeywords(
        dihedrals=[(0,1,2,3)],
        grid_spacing=[15],
    ),
)

# Two initial conformers for a single torsiondrive
meta, ids = client.add_torsiondrives(
    initial_molecules=[[mol_conf1, mol_conf2]],
    program='torsiondrive',
    optimization_specification=td_spec.optimization_specification,
    keywords=td_spec.keywords,
)

Working with Results#

When writing/reading keys (for example, when working with serialized data), use serialize_key() and deserialize_key().

Torsiondrive Datasets#

Torsiondrive datasets are collections of torsiondrive records. Entries contain one or more initial molecules, and the dataset specifications wrap a TorsiondriveSpecification.

Torsiondrive datasets differ from the other dataset types in one important way. For most dataset types the specification is self-contained and the entry supplies only the molecule. Here, the scan definition itself - which dihedral to drive - depends on the atom indices of the particular molecule, and those differ from entry to entry. So a torsiondrive entry carries keywords of its own, and the two are combined when records are created.

An entry has two keyword fields in addition to its molecules:

  • additional_keywords - merged into the specification’s torsiondrive TorsiondriveKeywords. This is where dihedrals and grid_spacing normally live

  • additional_optimization_keywords - merged into the specification’s optimization_specification.keywords

See Datasets for general dataset operations and advanced usage.

How entry and specification keywords are combined#

When a dataset is submitted, the server builds one torsiondrive specification for each (entry, specification) pair by taking the dataset specification and updating it from the entry:

Specification field

Updated from the entry’s

keywords

additional_keywords

optimization_specification.keywords

additional_optimization_keywords

The merge is a plain dictionary update, one level deep. A key present in the entry replaces that key in the specification outright; keys the entry does not mention are left as they were. Values are not combined element-wise - an entry that sets grid_spacing replaces the whole list, it does not append to it.

Nothing else in the specification is affected. The optimization_specification’s program, qc_specification, and protocols come from the dataset specification alone and cannot be varied per entry.

Because dihedrals is molecule-specific, the usual arrangement is:

  • The dataset specification holds the level of theory - the optimization program, the QC program, method, and basis - and leaves keywords empty

  • Each entry holds that molecule’s dihedrals and grid_spacing

Both halves are still ordinary keyword dictionaries, though, so anything that genuinely is common to the whole dataset can go in the specification instead and be inherited by every entry. Settings that are common to most entries but not all can be put in the specification and overridden by the handful of entries that differ. The examples below show each arrangement.

Warning

additional_keywords is stored as an unvalidated dictionary on the entry. It is only checked against TorsiondriveKeywords, which forbids unknown fields, at the point where the merged specification is built - that is, on submission, not when the entry is added.

>>> # Note the typo - 'grid_spacings'. This is accepted without complaint
>>> ds.add_entry("butane", [butane_conf1], additional_keywords={"grid_spacings": [15]})

Because that validation failure happens inside the server, it does not come back as a friendly message - on a server with the default hide_internal_errors setting you will get an internal server error and an error ID rather than the name of the offending key. If a submission fails for no apparent reason, check the spelling of the keys in your entries against the field names of TorsiondriveKeywords.

additional_optimization_keywords is not checked this way, since OptimizationSpecification.keywords is an open dictionary passed through to the optimization program. A misspelled key there is silently ignored, or rejected later by the optimizer itself.

Dataset Examples#

Create a torsiondrive dataset
ds = client.add_dataset("torsiondrive", "Torsion Scan Dataset", "A demonstration torsiondrive dataset")
Add a specification with empty keywords (the usual case)

The specification carries the level of theory only. keywords is left at its default, so every field of the scan definition comes from the entries.

from qcportal.torsiondrive import TorsiondriveSpecification, TorsiondriveKeywords
from qcportal.optimization import OptimizationSpecification
from qcportal.singlepoint import QCSpecification

td_spec = TorsiondriveSpecification(
    program="torsiondrive",
    optimization_specification=OptimizationSpecification(
        program="geometric",
        qc_specification=QCSpecification(program="psi4", method="b3lyp", basis="def2-svp", driver="deferred"),
    ),
    keywords=TorsiondriveKeywords(),
)

ds.add_specification("td-geometric/psi4-b3lyp-def2-svp", td_spec)
Add entries with per-entry dihedrals and grid spacing

Each entry drives a different dihedral, given as atom indices into that entry’s own molecules. This is the reason the scan definition lives on the entry rather than the specification.

# The central C-C torsion of butane, and the C-C torsion of ethane -
# different atom indices in each molecule
ds.add_entry(
    "butane",
    [butane_conf1, butane_conf2],
    additional_keywords={"dihedrals": [(0, 1, 2, 3)], "grid_spacing": [15]},
)

ds.add_entry(
    "ethane",
    [ethane_conf],
    additional_keywords={"dihedrals": [(2, 0, 1, 5)], "grid_spacing": [15]},
)

ds.submit()
Add many entries at once, building the entry objects directly
from qcportal.torsiondrive import TorsiondriveDatasetNewEntry

# Molecules and the dihedral to drive in each, worked out beforehand
to_scan = [
    ("butane", [butane_conf1, butane_conf2], (0, 1, 2, 3)),
    ("ethane", [ethane_conf], (2, 0, 1, 5)),
    ("propane", [propane_conf], (0, 1, 2, 6)),
]

entries = [
    TorsiondriveDatasetNewEntry(
        name=name,
        initial_molecules=mols,
        additional_keywords={"dihedrals": [dihedral], "grid_spacing": [15]},
    )
    for name, mols, dihedral in to_scan
]

# Efficiently add all entries in a single call
ds.add_entries(entries)
Put shared keywords in the specification instead

Options that are the same for every entry can go in the specification, where they only have to be written once. Here the spacing and range are shared, and the entries supply only dihedrals.

from qcportal.torsiondrive import TorsiondriveSpecification, TorsiondriveKeywords
from qcportal.optimization import OptimizationSpecification
from qcportal.singlepoint import QCSpecification

td_spec = TorsiondriveSpecification(
    program="torsiondrive",
    optimization_specification=OptimizationSpecification(
        program="geometric",
        qc_specification=QCSpecification(program="psi4", method="b3lyp", basis="def2-svp", driver="deferred"),
    ),
    keywords=TorsiondriveKeywords(
        grid_spacing=[15],
        dihedral_ranges=[(-165, 180)],
        energy_upper_limit=0.05,
    ),
)

ds.add_specification("coarse", td_spec)

# Entries only need to say which dihedral to drive
ds.add_entry("butane", [butane_conf1], additional_keywords={"dihedrals": [(0, 1, 2, 3)]})
ds.add_entry("ethane", [ethane_conf], additional_keywords={"dihedrals": [(2, 0, 1, 5)]})
Override a specification keyword for one entry

An entry’s additional_keywords replaces matching keys and leaves the rest alone, so a single entry can deviate from the dataset-wide settings. With the "coarse" specification above:

# Uses the specification's grid_spacing of 15
ds.add_entry("butane", [butane_conf1], additional_keywords={"dihedrals": [(0, 1, 2, 3)]})

# Scanned more finely, but still inherits dihedral_ranges and energy_upper_limit
ds.add_entry(
    "butane_fine",
    [butane_conf1],
    additional_keywords={"dihedrals": [(0, 1, 2, 3)], "grid_spacing": [5]},
)

The record created for butane_fine gets grid_spacing=[5], dihedral_ranges=[(-165, 180)], and energy_upper_limit=0.05.

Pass keywords through to the optimizer per entry

additional_optimization_keywords is merged into optimization_specification.keywords the same way, which is useful when one awkward molecule needs different optimizer settings.

td_spec = TorsiondriveSpecification(
    program="torsiondrive",
    optimization_specification=OptimizationSpecification(
        program="geometric",
        qc_specification=QCSpecification(program="psi4", method="b3lyp", basis="def2-svp", driver="deferred"),
        keywords={"maxiter": 200},
    ),
    keywords=TorsiondriveKeywords(grid_spacing=[15]),
)

ds.add_specification("default", td_spec)

# Inherits maxiter=200
ds.add_entry("butane", [butane_conf1], additional_keywords={"dihedrals": [(0, 1, 2, 3)]})

# Overrides maxiter and adds another optimizer keyword
ds.add_entry(
    "stubborn_molecule",
    [stubborn_conf],
    additional_keywords={"dihedrals": [(4, 5, 6, 7)]},
    additional_optimization_keywords={"maxiter": 500, "coordsys": "dlc"},
)

The record for stubborn_molecule is optimized with keywords={"maxiter": 500, "coordsys": "dlc"} - maxiter replaced, coordsys added.

Notes and Tips#

  • Each grid point corresponds to a full optimization record. You can access trajectory information and final structures/energies via those records.

  • Torsiondrive keys represent dihedral angle positions; when serialized to strings (eg in JSON), use helper functions to convert to/from tuples if needed.

  • See Singlepoint Specification (QCSpecification) and Optimization Specification for details on setting up the underlying QC and optimization parameters used at each grid point.

  • In a dataset, dihedrals belongs on the entry, not the specification - see How entry and specification keywords are combined. The finished record’s specification shows the merged result, so a record’s keywords will not look identical to the dataset specification it came from.

  • driver="deferred" is the conventional setting for the qc_specification inside an optimization specification, since the driver used at each step is decided by the optimizer.

Torsiondrive QCPortal API#