Objective Function¶
objective
¶
Objective function for force field optimization.
Wraps the ForceField ↔ MM-engine ↔ reference-data loop into a single
callable that :func:scipy.optimize.minimize can drive.
Scoring approach¶
This module uses raw weighted residuals (modern approach):
.. math:: r_i = w_i (x_{ref,i} - x_{calc,i})
The objective value is sum(r_i**2). This is the standard form expected
by scipy.optimize.least_squares and gradient-based minimizers.
The legacy code in q2mm.optimizers.scoring uses a different normalisation
inherited from upstream compare.py:
- Energies are zero-referenced via
correlate_energies()before scoring - A denominator based on the total count of energy-type data points is
applied (see
compare_data()).
For migration validation, use :func:q2mm.optimizers.scoring.compare_data
directly — it is importable and usable standalone to cross-check scores
against the upstream code path.
ReferenceValue
dataclass
¶
ReferenceValue(kind: Literal['energy', 'frequency', 'bond_length', 'bond_angle', 'torsion_angle', 'eig_diagonal', 'eig_offdiagonal'], value: float, weight: float = 1.0, label: str = '', molecule_idx: int = 0, data_idx: int = 0, atom_indices: tuple[int, ...] | None = None)
A single reference observation (QM or experimental).
ReferenceData
dataclass
¶
ReferenceData(values: list[ReferenceValue] = list())
Complete set of reference data for an optimization.
Each entry describes one observable: an energy, a frequency, or a geometric parameter that the force field should reproduce.
n_observations
property
¶
Total number of reference observations.
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
Length of the |
add_energy
¶
Add a single energy reference value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float
|
Reference energy value. |
required |
weight
|
float
|
Weight for this entry. |
1.0
|
molecule_idx
|
int
|
Index into the molecules list. |
0
|
label
|
str
|
Human-readable label. |
''
|
Source code in q2mm/optimizers/objective.py
add_frequency
¶
add_frequency(value: float, *, data_idx: int, weight: float = 1.0, molecule_idx: int = 0, label: str = '')
Add a single vibrational frequency reference value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float
|
Reference frequency in cm⁻¹. |
required |
data_idx
|
int
|
0-based index of this frequency mode. |
required |
weight
|
float
|
Weight for this entry. |
1.0
|
molecule_idx
|
int
|
Index into the molecules list. |
0
|
label
|
str
|
Human-readable label. |
''
|
Source code in q2mm/optimizers/objective.py
add_bond_length
¶
add_bond_length(value: float, *, data_idx: int = -1, atom_indices: tuple[int, int] | None = None, weight: float = 1.0, molecule_idx: int = 0, label: str = '')
Add a single bond length reference value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float
|
Reference bond length in Ångströms. |
required |
data_idx
|
int
|
0-based positional index (fallback if
|
-1
|
atom_indices
|
tuple[int, int] | None
|
Atom pair for identity-based matching. |
None
|
weight
|
float
|
Weight for this entry. |
1.0
|
molecule_idx
|
int
|
Index into the molecules list. |
0
|
label
|
str
|
Human-readable label. |
''
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither |
Source code in q2mm/optimizers/objective.py
add_bond_angle
¶
add_bond_angle(value: float, *, data_idx: int = -1, atom_indices: tuple[int, int, int] | None = None, weight: float = 1.0, molecule_idx: int = 0, label: str = '')
Add a single bond angle reference value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float
|
Reference bond angle in degrees. |
required |
data_idx
|
int
|
0-based positional index (fallback if
|
-1
|
atom_indices
|
tuple[int, int, int] | None
|
Atom triple (i, j, k) for identity-based matching. |
None
|
weight
|
float
|
Weight for this entry. |
1.0
|
molecule_idx
|
int
|
Index into the molecules list. |
0
|
label
|
str
|
Human-readable label. |
''
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither |
Source code in q2mm/optimizers/objective.py
add_torsion_angle
¶
add_torsion_angle(value: float, *, atom_indices: tuple[int, int, int, int], weight: float = 1.0, molecule_idx: int = 0, label: str = '')
Add a single torsion (dihedral) angle reference value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float
|
Reference torsion angle in degrees. |
required |
atom_indices
|
tuple[int, int, int, int]
|
Four atom indices defining the dihedral. |
required |
weight
|
float
|
Weight for this entry. |
1.0
|
molecule_idx
|
int
|
Index into the molecules list. |
0
|
label
|
str
|
Human-readable label. |
''
|
Source code in q2mm/optimizers/objective.py
add_hessian_eigenvalue
¶
add_hessian_eigenvalue(value: float, *, mode_idx: int, weight: float = 0.1, molecule_idx: int = 0, label: str = '')
Add a diagonal element (eigenvalue) of the eigenmatrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float
|
QM eigenvalue for this mode. |
required |
mode_idx
|
int
|
0-based index of the vibrational mode. |
required |
weight
|
float
|
Weight for this entry. Legacy defaults: 0.10 for both low- and high-frequency modes, 0.00 for the first (imaginary) mode. |
0.1
|
molecule_idx
|
int
|
Index into the molecules list. |
0
|
label
|
str
|
Human-readable label. |
''
|
Source code in q2mm/optimizers/objective.py
add_hessian_offdiagonal
¶
add_hessian_offdiagonal(value: float, *, row: int, col: int, weight: float = 0.05, molecule_idx: int = 0, label: str = '')
Add an off-diagonal element of the eigenmatrix.
Off-diagonal elements measure cross-coupling between modes. They should be close to zero when the MM Hessian closely reproduces the QM eigenvector structure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float
|
QM eigenmatrix element (typically 0.0 for the QM self-projection). |
required |
row
|
int
|
0-based row index into the eigenmatrix. |
required |
col
|
int
|
0-based column index into the eigenmatrix. |
required |
weight
|
float
|
Weight for this entry. Legacy default: 0.05. |
0.05
|
molecule_idx
|
int
|
Index into the molecules list. |
0
|
label
|
str
|
Human-readable label. |
''
|
Source code in q2mm/optimizers/objective.py
add_eigenmatrix_from_hessian
¶
add_eigenmatrix_from_hessian(hessian: ndarray, *, diagonal_only: bool = False, molecule_idx: int = 0, weights: dict[str, float] | None = None, skip_first: bool = True, eigenvalue_threshold: float = 0.1173) -> int
Bulk-load eigenmatrix training data from a QM Hessian.
Decomposes the Hessian, computes the eigenmatrix, and adds all elements as reference values with the legacy weight scheme.
The Hessian should be in canonical units (Hartree/Bohr²).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hessian
|
ndarray
|
QM Hessian matrix |
required |
diagonal_only
|
bool
|
If |
False
|
molecule_idx
|
int
|
Index into the molecules list. |
0
|
weights
|
dict[str, float] | None
|
Weight overrides. Keys:
|
None
|
skip_first
|
bool
|
If |
True
|
eigenvalue_threshold
|
float
|
Eigenvalue threshold (Hartree/Bohr²) separating low/high frequency modes for weight assignment. The default 0.1173 corresponds to the legacy threshold of 1100 kJ/(mol·Å²). |
0.1173
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
Number of entries added. |
Source code in q2mm/optimizers/objective.py
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 | |
add_frequencies_from_array
¶
add_frequencies_from_array(frequencies: ndarray | list[float], *, weight: float = 1.0, molecule_idx: int = 0, skip_imaginary: bool = False) -> int
Add all frequencies from a 1-D array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequencies
|
ndarray | list[float]
|
Vibrational frequencies (cm⁻¹). Imaginary modes should be negative values. |
required |
weight
|
float
|
Weight applied to every frequency entry. |
1.0
|
molecule_idx
|
int
|
Index into the molecules list for multi-structure fits. |
0
|
skip_imaginary
|
bool
|
If |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
Number of frequency entries added. |
Source code in q2mm/optimizers/objective.py
from_molecule
classmethod
¶
from_molecule(mol: Q2MMMolecule, *, weights: dict[str, float] | None = None, molecule_idx: int = 0, frequencies: ndarray | list[float] | None = None, skip_imaginary: bool = False, include_eigenmatrix: bool = False, eigenmatrix_diagonal_only: bool = False) -> ReferenceData
Auto-populate reference data from a molecule's detected geometry.
Extracts all auto-detected bond lengths and bond angles from the molecule. Optionally adds vibrational frequencies and/or Hessian eigenmatrix training data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mol
|
Q2MMMolecule
|
Molecule with geometry (bonds/angles auto-detected). |
required |
weights
|
dict[str, float] | None
|
Weight overrides keyed by
data type. Supported keys: |
None
|
molecule_idx
|
int
|
Index for multi-molecule fits. |
0
|
frequencies
|
ndarray | list[float] | None
|
Vibrational frequencies (cm⁻¹) to include. |
None
|
skip_imaginary
|
bool
|
If |
False
|
include_eigenmatrix
|
bool
|
If |
False
|
eigenmatrix_diagonal_only
|
bool
|
If |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
ReferenceData |
ReferenceData
|
Populated with bond lengths, angles, and (optionally) frequencies and eigenmatrix data. |
Source code in q2mm/optimizers/objective.py
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 | |
from_molecules
classmethod
¶
from_molecules(molecules: list[Q2MMMolecule], *, weights: dict[str, float] | None = None, frequencies_list: list[ndarray | list[float]] | None = None, skip_imaginary: bool = False, include_eigenmatrix: bool = False, eigenmatrix_diagonal_only: bool = False) -> ReferenceData
Auto-populate reference data from multiple molecules.
Each molecule is assigned a sequential molecule_idx starting
from 0. Delegates to :meth:from_molecule per molecule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
molecules
|
list[Q2MMMolecule]
|
Training set molecules. |
required |
weights
|
dict[str, float] | None
|
Weight overrides (same
keys as :meth: |
None
|
frequencies_list
|
list[ndarray | list[float]] | None
|
Per-molecule frequencies. Must have the same length as molecules if provided. |
None
|
skip_imaginary
|
bool
|
If |
False
|
include_eigenmatrix
|
bool
|
If |
False
|
eigenmatrix_diagonal_only
|
bool
|
If |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
ReferenceData |
ReferenceData
|
Combined reference data for all molecules. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in q2mm/optimizers/objective.py
from_gaussian
classmethod
¶
from_gaussian(path: str | Path, *, weights: dict[str, float] | None = None, bond_tolerance: float = 1.3, charge: int = 0, multiplicity: int = 1, include_frequencies: bool = True, skip_imaginary: bool = False, au_hessian: bool = True) -> tuple[ReferenceData, Q2MMMolecule]
Build reference data from a Gaussian log file.
Parses the log file for the optimised geometry and vibrational frequencies, then auto-populates bond lengths, angles, and (optionally) frequencies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Path to the Gaussian |
required |
weights
|
dict[str, float] | None
|
Weight overrides (same
keys as :meth: |
None
|
bond_tolerance
|
float
|
Multiplier for covalent-radii bond detection. Use 1.4+ for TS. |
1.3
|
charge
|
int
|
Molecular charge. |
0
|
multiplicity
|
int
|
Spin multiplicity. |
1
|
include_frequencies
|
bool
|
Whether to add frequency data from the log file. |
True
|
skip_imaginary
|
bool
|
If |
False
|
au_hessian
|
bool
|
Keep Hessian in atomic units (Hartree/Bohr²). |
True
|
Returns:
| Type | Description |
|---|---|
tuple[ReferenceData, Q2MMMolecule]
|
tuple[ReferenceData, Q2MMMolecule]: Populated reference data and the parsed molecule (with Hessian attached if available). |
Source code in q2mm/optimizers/objective.py
from_fchk
classmethod
¶
from_fchk(path: str | Path, *, weights: dict[str, float] | None = None, bond_tolerance: float = 1.3, charge: int = 0, multiplicity: int = 1) -> tuple[ReferenceData, Q2MMMolecule]
Build reference data from a Gaussian formatted checkpoint file.
Parses the .fchk file for geometry, Cartesian Force Constants
(Hessian), and atom data. Auto-populates bond lengths and angles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Path to the Gaussian |
required |
weights
|
dict[str, float] | None
|
Weight overrides (same
keys as :meth: |
None
|
bond_tolerance
|
float
|
Multiplier for covalent-radii bond detection. |
1.3
|
charge
|
int
|
Molecular charge (overridden by file values if present). |
0
|
multiplicity
|
int
|
Spin multiplicity (overridden by file values if present). |
1
|
Returns:
| Type | Description |
|---|---|
tuple[ReferenceData, Q2MMMolecule]
|
tuple[ReferenceData, Q2MMMolecule]: Populated reference data and the parsed molecule with Hessian. |
Source code in q2mm/optimizers/objective.py
ObjectiveFunction
¶
ObjectiveFunction(forcefield: ForceField, engine: MMEngine, molecules: list[Q2MMMolecule], reference: ReferenceData)
Objective function for scipy-based force field optimization.
Evaluates the weighted sum-of-squares between MM-calculated and reference data for one or more molecules.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forcefield
|
ForceField
|
The force field whose parameters are being optimized. |
required |
engine
|
MMEngine
|
The MM backend (OpenMM, Tinker, etc.). |
required |
molecules
|
list[Q2MMMolecule]
|
Training set molecules. |
required |
reference
|
ReferenceData
|
QM/experimental reference observations. |
required |
Initialize the objective function.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forcefield
|
ForceField
|
The force field whose parameters are being optimized. |
required |
engine
|
MMEngine
|
The MM backend (OpenMM, Tinker, etc.). |
required |
molecules
|
list[Q2MMMolecule]
|
Training set molecules. |
required |
reference
|
ReferenceData
|
QM/experimental reference observations. |
required |
Source code in q2mm/optimizers/objective.py
__call__
¶
Evaluate objective for a given parameter vector.
This is the function signature that :func:scipy.optimize.minimize
expects: f(x) -> float.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
param_vector
|
ndarray
|
Flat parameter vector. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Sum-of-squared weighted residuals. |
Source code in q2mm/optimizers/objective.py
residuals
¶
Compute weighted residual vector (for least-squares methods).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
param_vector
|
ndarray
|
Flat parameter vector. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
np.ndarray: Weighted residuals for each reference observation. |
Source code in q2mm/optimizers/objective.py
gradient
¶
Compute analytical gradient of the score w.r.t. parameters.
Uses the engine's energy_and_param_grad() method (available on
:class:~q2mm.backends.mm.jax_engine.JaxEngine) to compute exact
derivatives for energy reference data. Raises NotImplementedError
for reference data types that require Hessians or minimized geometries;
use jac=None (finite differences) for mixed reference data.
The score is sum_i (w_i * (ref_i - calc_i))**2, so:
d(score)/d(p) = -2 * sum_i [w_i^2 * (ref_i - calc_i) * d(calc_i)/d(p)]
Note
This method does not increment n_eval or append to
history. SciPy's minimize calls fun(x) and jac(x)
separately, so tracking state here would double-count evaluations.
Evaluation counting is handled exclusively in __call__.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
param_vector
|
ndarray
|
Flat parameter vector (same as
:meth: |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
np.ndarray: Gradient of the score with respect to each parameter. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the engine does not support
|
NotImplementedError
|
If the reference data contains types
other than |
Source code in q2mm/optimizers/objective.py
reset
¶
Reset evaluation counter, history, and cached engine handles.