Relaxed coordinate scans¶
vq.relaxed_scan(...) drives one internal coordinate — a bond, an
angle, or a dihedral — across a sequence of target values and relaxes
all other degrees of freedom at each step. This is the canonical
“scan the reaction coordinate” workflow used to locate transition-state
guesses and map reaction energy profiles. It works for both molecules
and periodic systems with the same API.
Quick start — molecular O–H stretch¶
import numpy as np
import vibeqc as vq
h2o = vq.Molecule(
[
vq.Atom(8, [0.00, 0.00, 0.0]),
vq.Atom(1, [1.81, 0.00, 0.0]),
vq.Atom(1, [-0.46, 1.75, 0.0]),
],
charge=0, multiplicity=1,
)
result = vq.relaxed_scan(
h2o, "sto-3g",
coordinate=("bond", 0, 1), # O (atom 0) → H (atom 1)
values=np.linspace(1.6, 2.4, 9), # bohr
method="RHF",
output="oh_stretch", # writes oh_stretch.qvf
)
print(result.values) # target bond lengths
print(result.energies) # relaxed energies (Ha)
print(result.converged_flags) # bool per scan point
print(result.geometries[0]) # vq.Molecule at the first scan point
The optimizer freezes the two constraint atoms (so the bond length stays exactly at the target) and lets every other atom relax. Each scan point warm-starts from the previous relaxed geometry, which gives smoother paths.
Quick start — periodic bond scan¶
The same call signature works for periodic systems; pass a
PeriodicSystem and a kpoints mesh.
import numpy as np
import vibeqc as vq
L = np.eye(3) * 12.0
h2_box = vq.PeriodicSystem(
3, L,
[vq.Atom(1, [6.0, 6.0, 6.0]),
vq.Atom(1, [7.4, 6.0, 6.0])],
charge=0, multiplicity=1,
)
result = vq.relaxed_scan(
h2_box, "sto-3g",
coordinate=("bond", 0, 1),
values=np.linspace(1.2, 2.4, 7),
method="RHF",
kpoints=(1, 1, 1), # Γ-only
output="h2_dissociation",
)
Coordinate types¶
Three coordinate kinds are supported. Bond targets are in bohr; angle and dihedral targets are in radians.
Coordinate |
Form |
Unit |
|---|---|---|
Bond length |
|
bohr |
Bond angle (vertex |
|
radians |
Dihedral (i–j–k–l torsion) |
|
radians |
Atom indices are 0-based and refer to the order of atoms in
molecule.atoms (or system.unit_cell for periodic systems).
Freezing additional atoms — the slab pattern¶
For surface catalysis, freeze the slab bottom while the adsorbate
moves. The constraint atoms are always frozen; pass extra indices via
freeze_indices:
slab_info = vq.slab(...) # see slabs_and_adsorbates.md
n_layers_frozen = 2
result = vq.relaxed_scan(
slab_info.system, "def2-svp",
coordinate=("bond", n2_idx_a, n2_idx_b), # N2 bond stretch
values=np.linspace(2.1, 4.0, 12),
method="RKS", functional="PBE",
kpoints=(2, 2, 1),
freeze_indices=slab_info.bottom_layer_indices(n_layers_frozen),
output="n2_dissociation_fe100",
)
The frozen-atom forwarding mirrors vibeqc.relax_atoms() (see
CRYSTAL-style BIPOLE periodic HF and DFT).
Approach: rigid step with frozen endpoints¶
At each scan point:
The starting geometry is mutated so the constrained coordinate hits the target value exactly. For a bond, atom
jis moved along thei → jdirection. For an angle, atomkis rotated about vertexj. For a dihedral, atomlis rotated about thej → kaxis.The atoms participating in the constraint are added to the optimizer’s
freeze_indices. The remaining atoms relax around the constrained geometry using the existing analytic-gradient L-BFGS-B driver.The resulting geometry, energy, and convergence flag are recorded.
This is the same “rigid step” approach used by most QC packages for relaxed scans. The slightly more elegant alternative — projecting the gradient orthogonal to the constraint direction — is a planned v1.0 enhancement.
Output: ScanResult and the QVF reaction path¶
relaxed_scan returns a vibeqc.ScanResult:
result.values # np.ndarray of target coordinate values
result.energies # np.ndarray of relaxed energies (Ha)
result.geometries # list of Molecule / PeriodicSystem
result.converged_flags # list of bool
result.coordinate # the constraint spec passed in
result.output_path # Path to the QVF, if output= was set
When output="..." is set (or you call result.write_qvf(path)),
a vibe-view-compatible reaction.path QVF is emitted. Frames become
the scan animation; the reaction coordinate axis is the constraint
value; endpoints are tagged as reactant / product waypoints, and the
highest-energy frame is tagged as a generic point waypoint. (It is
not tagged as a transition state — that requires a Hessian check;
see hessians.)
Open the resulting .qvf with vibe-view to scrub the scan
interactively.
Periodic scans → QVF v2. When geometries is a list of
PeriodicSystem (periodic scan), the archive ships as
qvf_version: 2 with the per-frame lattice + dim attached to the
reaction.path section. vibe-view’s renderer (QVF Inc B) draws
the unit-cell wireframe and wraps atom positions across in-plane
periodic boundaries — slab scans animate with the cell visible
and adsorbates don’t jump across the cell boundary mid-scan.
Citation surface. write_qvf emits a citations section
containing the BibTeX for every per-frame SCF (method / basis /
functional). The relaxed-scan driver itself doesn’t fire a
driver-specific paper (relaxed coordinate scans are textbook;
there is no single “scan paper” to cite). NEB callers needing
the Henkelman+Jónsson 2000 / Henkelman+Uberuaga+Jónsson 2000 /
Smidstrup 2014 citations should use
vibeqc.NEBResult.write_qvf() instead — it fires the
routes.drivers.neb (and optionally ci_neb) routes
automatically.
Limits and out-of-scope features¶
2D scans (sweeping two coordinates simultaneously) are not yet supported. Run two nested 1D scans by hand for now.
Constrained-gradient projection is not implemented; the rigid- step approach above is what ships today.
Synchronous-transit / NEB path methods live in a separate workstream —
relaxed_scanis the simpler reaction-coordinate driver.