Skip to content

Backend API Reference

The backend module provides interfaces for integrating GADES with different molecular dynamics engines.

Overview

GADES supports multiple simulation backends:

  • OpenMMBackend: For OpenMM-based simulations
  • ASEBackend: For ASE (Atomic Simulation Environment) based simulations

ASE Backend

The ASE backend allows GADES to work with any calculator supported by ASE, including LAMMPS, VASP, Quantum ESPRESSO, and many others.

Quick Start with with_gades Factory Method

The recommended way to create an ASE backend with GADES bias is using the with_gades factory method:

from GADES.backend import ASEBackend
from GADES.utils import compute_hessian_force_fd_richardson as hessian

backend = ASEBackend.with_gades(
    atoms=atoms,
    base_calc=lammps_calc,
    bias_atom_indices=biasing_atom_ids,
    hess_func=hessian,
    clamp_magnitude=1000,
    kappa=0.9,
    interval=100,
    stability_interval=1000,
)

# Attach integrator for step tracking
backend.integrator = dyn

# Access GADESBias if needed
print(backend.gades_bias.kappa)

Understanding the Architecture

The ASE integration involves three interconnected components:

┌─────────────────┐     ┌──────────────────┐     ┌─────────────┐
│   GADESBias     │────▶│  GADESCalculator │────▶│  ASEBackend │
│                 │◀────│                  │◀────│             │
│ Computes bias   │     │ Wraps base calc  │     │ Manages     │
│ forces along    │     │ and adds GADES   │     │ atoms and   │
│ softest mode    │     │ bias to forces   │     │ state       │
└─────────────────┘     └──────────────────┘     └─────────────┘
        │                                               │
        └───────────────────────────────────────────────┘
                    Circular reference

This creates a circular dependency at initialization time:

  1. GADESBias needs a Backend to query forces and positions
  2. GADESCalculator needs a GADESBias to compute bias forces
  3. ASEBackend needs a GADESCalculator to wrap

The with_gades factory method solves this by handling the wiring internally.

API Reference

Backend

Backend()

A generic interface for the backends to be used with GADES.

Subclasses must implement all methods that raise NotImplementedError. Methods with default implementations (is_stable, get_currentStep) may be overridden if the backend provides more accurate information.

Source code in GADES/backend.py
40
41
def __init__(self) -> None:
    self.name = ""

is_stable

is_stable()

Check if the simulation is numerically stable.

Returns:

Name Type Description
bool bool

True if stable. Default implementation always returns True.

Source code in GADES/backend.py
43
44
45
46
47
48
49
50
def is_stable(self) -> bool:
    """
    Check if the simulation is numerically stable.

    Returns:
        bool: True if stable. Default implementation always returns True.
    """
    return True

get_currentStep

get_currentStep()

Get the current simulation step number.

Returns:

Name Type Description
int int

Current step count. Default implementation returns 0.

Source code in GADES/backend.py
52
53
54
55
56
57
58
59
def get_currentStep(self) -> int:
    """
    Get the current simulation step number.

    Returns:
        int: Current step count. Default implementation returns 0.
    """
    return 0

get_atom_symbols

get_atom_symbols(bias_atom_indices)

Get the chemical symbols for the specified atoms.

Parameters:

Name Type Description Default
bias_atom_indices Sequence[int]

Indices of atoms to get symbols for.

required

Returns:

Type Description
List[str]

List[str]: Chemical symbols for each atom.

Source code in GADES/backend.py
61
62
63
64
65
66
67
68
69
70
71
def get_atom_symbols(self, bias_atom_indices: Sequence[int]) -> List[str]:
    """
    Get the chemical symbols for the specified atoms.

    Args:
        bias_atom_indices: Indices of atoms to get symbols for.

    Returns:
        List[str]: Chemical symbols for each atom.
    """
    raise NotImplementedError

get_positions

get_positions()

Retrieve the current atom positions.

Returns:

Type Description
ndarray

np.ndarray: Positions array with shape (N, 3).

Source code in GADES/backend.py
73
74
75
76
77
78
79
80
def get_positions(self) -> np.ndarray:
    """
    Retrieve the current atom positions.

    Returns:
        np.ndarray: Positions array with shape ``(N, 3)``.
    """
    raise NotImplementedError

get_current_state

get_current_state()

Get the current positions and forces.

Returns:

Type Description
Tuple[ndarray, ndarray]

Tuple[np.ndarray, np.ndarray]: (positions, forces) arrays.

Source code in GADES/backend.py
82
83
84
85
86
87
88
89
def get_current_state(self) -> Tuple[np.ndarray, np.ndarray]:
    """
    Get the current positions and forces.

    Returns:
        Tuple[np.ndarray, np.ndarray]: (positions, forces) arrays.
    """
    raise NotImplementedError

get_forces

get_forces(positions)

Compute forces at the given positions.

Parameters:

Name Type Description Default
positions ndarray

Atom positions with shape (N, 3).

required

Returns:

Type Description
ndarray

np.ndarray: Flattened forces array with shape (3*N,).

Source code in GADES/backend.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def get_forces(self, positions: np.ndarray) -> np.ndarray:
    """
    Compute forces at the given positions.

    Args:
        positions: Atom positions with shape ``(N, 3)``.

    Returns:
        np.ndarray: Flattened forces array with shape ``(3*N,)``.
    """
    raise NotImplementedError

apply_bias

apply_bias(bias_force_object, biased_force_values, bias_atom_indices)

Apply bias forces to the specified atoms.

Parameters:

Name Type Description Default
bias_force_object Any

Backend-specific force object.

required
biased_force_values ndarray

Bias force values with shape (M, 3).

required
bias_atom_indices Sequence[int]

Indices of atoms to bias.

required
Source code in GADES/backend.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def apply_bias(
    self,
    bias_force_object: Any,
    biased_force_values: np.ndarray,
    bias_atom_indices: Sequence[int],
) -> None:
    """
    Apply bias forces to the specified atoms.

    Args:
        bias_force_object: Backend-specific force object.
        biased_force_values: Bias force values with shape ``(M, 3)``.
        bias_atom_indices: Indices of atoms to bias.
    """
    raise NotImplementedError

remove_bias

remove_bias(bias_force_object, bias_atom_indices)

Remove bias forces from the specified atoms.

Parameters:

Name Type Description Default
bias_force_object Any

Backend-specific force object.

required
bias_atom_indices Sequence[int]

Indices of atoms to unbias.

required
Source code in GADES/backend.py
119
120
121
122
123
124
125
126
127
128
129
def remove_bias(
    self, bias_force_object: Any, bias_atom_indices: Sequence[int]
) -> None:
    """
    Remove bias forces from the specified atoms.

    Args:
        bias_force_object: Backend-specific force object.
        bias_atom_indices: Indices of atoms to unbias.
    """
    raise NotImplementedError

OpenMMBackend

OpenMMBackend(simulation, target_temperature=None)

Bases: Backend

A wrapper for OpenMM to be used as a backend for GADES.

Parameters:

Name Type Description Default
simulation Simulation

The OpenMM Simulation object.

required
target_temperature Optional[float]

Target temperature in Kelvin for stability checking. If not provided, the backend will attempt to read it from the integrator (works for LangevinIntegrator, LangevinMiddleIntegrator, etc.). If neither is available, stability checking will be skipped with a warning.

None

Raises:

Type Description
ImportError

If OpenMM is not installed.

Source code in GADES/backend.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def __init__(
    self,
    simulation: "openmm.app.Simulation",  # type: ignore[name-defined]
    target_temperature: Optional[float] = None,
) -> None:
    if not _OPENMM_AVAILABLE:
        raise ImportError(
            "OpenMM is required for OpenMMBackend. "
            "Install with: conda install -c conda-forge openmm"
        )
    self.simulation = simulation
    self.system = self.simulation.system
    self.name = "openmm"
    self.target_temperature = target_temperature
    self._stability_warning_issued = False

is_stable

is_stable()

Check if the simulation is stable by comparing instantaneous temperature to the target temperature.

This method estimates the instantaneous temperature from the system's kinetic energy and compares it to the target temperature. If the deviation exceeds the stability threshold (configurable via defaults["stability_threshold_temp_diff"]), the system is considered unstable.

If no target temperature is available (not set explicitly and cannot be read from the integrator), a warning is issued once and the method returns True (stability check skipped).

Returns:

Name Type Description
bool bool

True if stable or if stability check is skipped, False if unstable.

Note
  • For a small number of biased DOFs, this criterion might give false positives.
  • The DOF calculation accounts for particles with mass, constraints, and CMMotionRemover. It may be inaccurate for systems with virtual sites, rigid bodies, barostats, or other motion removers. For such systems, set target_temperature explicitly in the constructor.
Source code in GADES/backend.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def is_stable(self) -> bool:
    """
    Check if the simulation is stable by comparing instantaneous temperature
    to the target temperature.

    This method estimates the instantaneous temperature from the system's
    kinetic energy and compares it to the target temperature. If the deviation
    exceeds the stability threshold (configurable via
    ``defaults["stability_threshold_temp_diff"]``), the system is considered unstable.

    If no target temperature is available (not set explicitly and cannot be read
    from the integrator), a warning is issued once and the method returns True
    (stability check skipped).

    Returns:
        bool: True if stable or if stability check is skipped, False if unstable.

    Note:
        - For a small number of biased DOFs, this criterion might give false positives.
        - The DOF calculation accounts for particles with mass, constraints, and
          CMMotionRemover. It may be inaccurate for systems with virtual sites,
          rigid bodies, barostats, or other motion removers. For such systems,
          set ``target_temperature`` explicitly in the constructor.
    """
    target_temp = self._get_target_temperature()

    if target_temp is None:
        if not self._stability_warning_issued:
            warnings.warn(
                "OpenMMBackend: Cannot perform stability check - no target temperature available. "
                "Either set target_temperature in OpenMMBackend constructor or use an NVT integrator "
                "(e.g., LangevinIntegrator). Stability checking will be skipped.",
                UserWarning
            )
            self._stability_warning_issued = True
        return True

    # Calculate degrees of freedom
    dof = 0
    for i in range(self.system.getNumParticles()):
        if self.system.getParticleMass(i) > 0*_unit.dalton:
            dof += 3
    for i in range(self.system.getNumConstraints()):
        p1, p2, distance = self.system.getConstraintParameters(i)
        if self.system.getParticleMass(p1) > 0*_unit.dalton or self.system.getParticleMass(p2) > 0*_unit.dalton:
            dof -= 1
    if any(type(self.system.getForce(i)) == _CMMotionRemover for i in range(self.system.getNumForces())):
        dof -= 3

    # Guard against invalid DOF (e.g., all virtual sites, misconfigured system)
    if dof <= 0:
        if not self._stability_warning_issued:
            warnings.warn(
                "OpenMMBackend: Cannot compute temperature - DOF <= 0. "
                "This may occur with virtual sites or misconfigured systems. "
                "Stability checking will be skipped.",
                UserWarning
            )
            self._stability_warning_issued = True
        return True

    # Calculate instantaneous temperature
    state = self.simulation.context.getState(getEnergy=True)
    current_temp = (2*state.getKineticEnergy()/(dof*_unit.MOLAR_GAS_CONSTANT_R)).value_in_unit(_unit.kelvin)

    threshold = defaults["stability_threshold_temp_diff"]
    if abs(current_temp - target_temp) > threshold:
        return False
    return True

get_positions

get_positions()

Retrieve the current atom positions from the OpenMM context.

Source code in GADES/backend.py
278
279
280
281
282
def get_positions(self) -> np.ndarray:
    """Retrieve the current atom positions from the OpenMM context."""
    state = self.simulation.context.getState(getPositions=True)
    positions = state.getPositions(asNumpy=True)
    return positions.value_in_unit(openmm.unit.nanometer)

get_forces

get_forces(positions)

Compute the original (unbiased) forces from an OpenMM context (internal use only).

This function updates the context with the provided positions, then retrieves forces from force group 0 only. Group 0 is assumed to contain the system's physical potential (e.g., the force field / PMF), while GADES bias forces are assigned to a separate group (default: group 1, configurable via defaults["gades_force_group"]).

Parameters:

Name Type Description Default
positions ndarray

Atomic positions, shaped (N, 3) in nanometers.

required

Returns:

Type Description
ndarray

Flattened force vector of shape (3 * N,), in units of kJ/mol/nm.

Notes

Original positions are restored after force computation.

Important

Force group 0 is hard-coded (see line with groups={0} below). If your simulation has physical forces in other groups (e.g., groups 2, 3), they will be excluded from Hessian and bias calculations. For most OpenMM simulations this is not an issue since all forces default to group 0.

If you need to include forces from multiple groups, modify the groups={0} parameter in this method to include all relevant groups, excluding only the GADES force group.

Source code in GADES/backend.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
def get_forces(self, positions: np.ndarray) -> np.ndarray:
    """
    Compute the original (unbiased) forces from an OpenMM context (internal use only).

    This function updates the context with the provided positions, then retrieves
    forces from force group 0 only. Group 0 is assumed to contain the system's
    physical potential (e.g., the force field / PMF), while GADES bias forces are
    assigned to a separate group (default: group 1, configurable via
    ``defaults["gades_force_group"]``).

    Args:
        positions: Atomic positions, shaped ``(N, 3)`` in nanometers.

    Returns:
        Flattened force vector of shape ``(3 * N,)``, in units of kJ/mol/nm.

    Notes:
        Original positions are restored after force computation.

    Important:
        **Force group 0 is hard-coded** (see line with ``groups={0}`` below).
        If your simulation has physical forces in other groups (e.g., groups 2, 3),
        they will be excluded from Hessian and bias calculations. For most OpenMM
        simulations this is not an issue since all forces default to group 0.

        If you need to include forces from multiple groups, modify the
        ``groups={0}`` parameter in this method to include all relevant groups,
        excluding only the GADES force group.
    """
    # Save original positions
    original_positions = self.get_positions()

    positions = positions * openmm.unit.nanometer
    self.simulation.context.setPositions(positions)
    # the `groups` keyword makes sure we're only capturing the forces from the
    # original pmf and not the biased one.
    state = self.simulation.context.getState(getForces=True, groups={0})
    forces = state.getForces(asNumpy=True).value_in_unit(
        openmm.unit.kilojoule_per_mole / openmm.unit.nanometer)

    # Restore original positions
    self.simulation.context.setPositions(original_positions * openmm.unit.nanometer)

    return forces.flatten()

apply_bias

apply_bias(bias_force_object, biased_force_values, bias_atom_indices)

Apply the bias forces to the specified atoms in the OpenMM simulation.

Parameters:

Name Type Description Default
bias_force_object CustomExternalForce

CustomExternalForce object.

required
biased_force_values ndarray

Array of bias forces, shape (N_biased, 3).

required
bias_atom_indices Sequence[int]

List of atom indices to apply the bias to.

required
Source code in GADES/backend.py
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
def apply_bias(
    self,
    bias_force_object: "openmm.CustomExternalForce",  # type: ignore[name-defined]
    biased_force_values: np.ndarray,
    bias_atom_indices: Sequence[int],
) -> None:
    """
    Apply the bias forces to the specified atoms in the OpenMM simulation.

    Args:
        bias_force_object: CustomExternalForce object.
        biased_force_values: Array of bias forces, shape ``(N_biased, 3)``.
        bias_atom_indices: List of atom indices to apply the bias to.
    """
    for i, idx in enumerate(bias_atom_indices):
        bias_force_object.setParticleParameters(idx, idx, tuple(biased_force_values[i]))
    bias_force_object.updateParametersInContext(self.simulation.context)

remove_bias

remove_bias(bias_force_object, bias_atom_indices)

Remove the bias forces from the specified atoms in the OpenMM simulation.

Parameters:

Name Type Description Default
bias_force_object CustomExternalForce

CustomExternalForce object.

required
bias_atom_indices Sequence[int]

List of atom indices to remove the bias from.

required
Source code in GADES/backend.py
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def remove_bias(
    self,
    bias_force_object: "openmm.CustomExternalForce",  # type: ignore[name-defined]
    bias_atom_indices: Sequence[int],
) -> None:
    """
    Remove the bias forces from the specified atoms in the OpenMM simulation.

    Args:
        bias_force_object: CustomExternalForce object.
        bias_atom_indices: List of atom indices to remove the bias from.
    """
    for idx in bias_atom_indices:
        bias_force_object.setParticleParameters(idx, idx, (0.0, 0.0, 0.0))
    bias_force_object.updateParametersInContext(self.simulation.context)

ASEBackend

ASEBackend(calculator, atoms, target_temperature=None)

Bases: Backend

A wrapper for ASE to be used as a backend for GADES.

Parameters:

Name Type Description Default
calculator GADESCalculator

The custom ASE Calculator (i.e. GADESCalculator) that includes GADES bias forces.

required
atoms Atoms

The ASE Atoms object.

required
target_temperature Optional[float]

Target temperature in Kelvin for stability checking. It is strongly recommended to set this explicitly. If not provided, the backend will attempt to read it from the integrator (works for Langevin, NVTBerendsen, NPTBerendsen), but this is fragile because ASE integrators use different attribute names and units internally. If no temperature can be determined, stability checking will be skipped with a warning.

None

Raises:

Type Description
ImportError

If ASE is not installed.

Source code in GADES/backend.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
def __init__(
    self,
    calculator: GADESCalculator,
    atoms: "Atoms",
    target_temperature: Optional[float] = None,
) -> None:
    if not _ASE_AVAILABLE:
        raise ImportError("ASE is required for ASEBackend. Install with: pip install ase")
    self.base_calc = calculator.base_calc
    self.atoms = atoms
    atoms.calc = calculator
    self.name = "ase"

    self.integrator = None
    self.current_step = -1
    self.target_temperature = target_temperature
    self._stability_warning_issued = False
    self.gades_bias = None

get_atoms

get_atoms()

Return the list of atoms in the ASE Atoms object.

Each entry is an Atom object, e.g. Atom('Ar', [0.0, 0.0, 0.0], index=0).

Source code in GADES/backend.py
601
602
603
604
605
606
607
608
def get_atoms(self) -> List[Any]:
    """
    Return the list of atoms in the ASE Atoms object.

    Each entry is an Atom object, e.g.
    ``Atom('Ar', [0.0, 0.0, 0.0], index=0)``.
    """
    return list(self.atoms)

get_atom_symbols

get_atom_symbols(bias_atom_indices)

Return the atomic symbols for the specified atom indices.

Source code in GADES/backend.py
610
611
612
613
614
615
616
617
618
619
def get_atom_symbols(self, bias_atom_indices: Sequence[int]) -> List[str]:
    """
    Return the atomic symbols for the specified atom indices.
    """
    atom_list = list(self.atoms)
    atom_symbols = [
        atom_list[i].symbol if atom_list[i].symbol is not None else "X"
        for i in bias_atom_indices
    ]
    return atom_symbols

get_currentStep

get_currentStep()

Return the current MD step from the integrator.

Returns:

Type Description
int

Current step number, or -1 if there is no integrator associated

int

with the backend.

Source code in GADES/backend.py
621
622
623
624
625
626
627
628
629
630
631
632
633
def get_currentStep(self) -> int:
    """
    Return the current MD step from the integrator.

    Returns:
        Current step number, or -1 if there is no integrator associated
        with the backend.
    """
    if self.integrator is not None:
        self.current_step = self.integrator.nsteps
    else:
        self.current_step = -1
    return self.current_step

get_positions

get_positions()

Retrieve the current atom positions.

Source code in GADES/backend.py
635
636
637
def get_positions(self) -> np.ndarray:
    """Retrieve the current atom positions."""
    return self.atoms.get_positions()

get_current_state

get_current_state()

Retrieve the current atom positions and total forces (including bias).

Returns:

Type Description
Tuple[ndarray, ndarray]

Tuple of (positions, forces) arrays, both with shape (N, 3).

Source code in GADES/backend.py
639
640
641
642
643
644
645
646
647
648
def get_current_state(self) -> Tuple[np.ndarray, np.ndarray]:
    """
    Retrieve the current atom positions and total forces (including bias).

    Returns:
        Tuple of (positions, forces) arrays, both with shape ``(N, 3)``.
    """
    positions = self.atoms.get_positions()
    forces = self.atoms.get_forces()  # Total forces via GADESCalculator
    return positions, forces

get_forces

get_forces(positions)

Compute the original (unbiased) forces from the base calculator.

This function updates the base calculator with the provided positions, then retrieves forces. The forces are flattened into a 1D array.

Parameters:

Name Type Description Default
positions ndarray

Atomic positions, shaped (N, 3).

required

Returns:

Type Description
ndarray

np.ndarray: Flattened force vector of shape (3*N,).

Notes

Original positions are restored after force computation.

Source code in GADES/backend.py
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
def get_forces(self, positions: np.ndarray) -> np.ndarray:
    """
    Compute the original (unbiased) forces from the base calculator.

    This function updates the base calculator with the provided positions,
    then retrieves forces. The forces are flattened into a 1D array.

    Args:
        positions (np.ndarray):
            Atomic positions, shaped ``(N, 3)``.

    Returns:
        np.ndarray: Flattened force vector of shape ``(3*N,)``.

    Notes:
        Original positions are restored after force computation.
    """
    # Save original positions
    original_positions = self.atoms.get_positions()

    self.atoms.set_positions(positions)
    self.base_calc.calculate(atoms=self.atoms, properties=['forces'], system_changes=_all_changes)
    forces = self.base_calc.results['forces']

    # Restore original positions
    self.atoms.set_positions(original_positions)

    return forces.flatten()

is_stable

is_stable()

Check if the simulation is stable by comparing instantaneous temperature to the target temperature.

The system is considered unstable if the temperature deviates by more than defaults["stability_threshold_temp_diff"] from the target. If no target temperature is available (not set explicitly and cannot be read from the integrator), a warning is issued once and the method returns True (stability check skipped).

Returns:

Name Type Description
bool bool

True if stable or if stability check is skipped, False if unstable.

Source code in GADES/backend.py
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
def is_stable(self) -> bool:
    """
    Check if the simulation is stable by comparing instantaneous temperature
    to the target temperature.

    The system is considered unstable if the temperature deviates by more than
    ``defaults["stability_threshold_temp_diff"]`` from the target. If no target
    temperature is available (not set explicitly and cannot be read from the
    integrator), a warning is issued once and the method returns True (stability
    check skipped).

    Returns:
        bool: True if stable or if stability check is skipped, False if unstable.
    """
    target_temp = self._get_target_temperature()

    if target_temp is None:
        if not self._stability_warning_issued:
            warnings.warn(
                "ASEBackend: Cannot perform stability check - no target temperature available. "
                "Either set target_temperature in ASEBackend constructor or use an NVT/NPT integrator. "
                "Stability checking will be skipped.",
                UserWarning
            )
            self._stability_warning_issued = True
        return True

    current_temp = self.atoms.get_temperature()

    threshold = defaults["stability_threshold_temp_diff"]
    if abs(current_temp - target_temp) > threshold:
        return False
    return True

apply_bias

apply_bias(bias_force_object, biased_force_values, bias_atom_indices)

Apply bias forces by storing them in GADESCalculator.

This method stores the bias values in the calculator's internal state, which will be added to forces during each calculate() call.

Parameters:

Name Type Description Default
bias_force_object Any

Unused for ASE (exists for API compatibility).

required
biased_force_values ndarray

Bias force values with shape (M, 3).

required
bias_atom_indices Sequence[int]

Indices of atoms to bias.

required
Source code in GADES/backend.py
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
def apply_bias(
    self,
    bias_force_object: Any,
    biased_force_values: np.ndarray,
    bias_atom_indices: Sequence[int],
) -> None:
    """
    Apply bias forces by storing them in GADESCalculator.

    This method stores the bias values in the calculator's internal state,
    which will be added to forces during each calculate() call.

    Args:
        bias_force_object: Unused for ASE (exists for API compatibility).
        biased_force_values: Bias force values with shape ``(M, 3)``.
        bias_atom_indices: Indices of atoms to bias.
    """
    calc = self.atoms.calc
    n_atoms = len(self.atoms)
    calc._stored_bias = np.zeros((n_atoms, 3))
    calc._stored_bias[bias_atom_indices, :] = biased_force_values
    calc._bias_active = True

remove_bias

remove_bias(bias_force_object, bias_atom_indices)

Remove bias forces by clearing GADESCalculator's stored bias.

Parameters:

Name Type Description Default
bias_force_object Any

Unused for ASE (exists for API compatibility).

required
bias_atom_indices Sequence[int]

Unused for ASE (exists for API compatibility).

required
Source code in GADES/backend.py
783
784
785
786
787
788
789
790
791
792
793
794
795
def remove_bias(
    self, bias_force_object: Any, bias_atom_indices: Sequence[int]
) -> None:
    """
    Remove bias forces by clearing GADESCalculator's stored bias.

    Args:
        bias_force_object: Unused for ASE (exists for API compatibility).
        bias_atom_indices: Unused for ASE (exists for API compatibility).
    """
    calc = self.atoms.calc
    calc._stored_bias = None
    calc._bias_active = False

with_gades classmethod

with_gades(atoms, base_calc, bias_atom_indices, hess_func, clamp_magnitude, kappa, interval, stability_interval=None, logfile_prefix=None, eigensolver='numpy', lanczos_iterations=None, use_bofill_update=False, full_hessian_interval=None, hvp_epsilon=None, target_temperature=None)

Factory method to create an ASEBackend with GADES bias fully configured.

This method handles all the internal wiring between GADESBias, GADESCalculator, and ASEBackend, eliminating the need for manual post-initialization patching.

Parameters:

Name Type Description Default
atoms Atoms

The ASE Atoms object.

required
base_calc Calculator

The base ASE Calculator (e.g., LAMMPS, EMT).

required
bias_atom_indices Sequence[int]

Indices of atoms that should receive the bias force.

required
hess_func Callable

Function to compute the Hessian matrix.

required
clamp_magnitude float

Maximum magnitude for bias force components.

required
kappa float

Scaling factor (0 < κ < 1) for the bias force.

required
interval int

Number of steps between bias force updates.

required
stability_interval Optional[int]

Steps between stability checks (optional).

None
logfile_prefix Optional[str]

Prefix for log files (optional).

None
eigensolver str

Method for computing softest eigenmode ('numpy', 'lanczos', or 'lanczos_hvp').

'numpy'
lanczos_iterations Optional[int]

Number of Lanczos iterations (if using 'lanczos' or 'lanczos_hvp').

None
use_bofill_update bool

Whether to use Bofill Hessian updates.

False
full_hessian_interval Optional[int]

Steps between full Hessian recomputation (if using Bofill).

None
hvp_epsilon Optional[float]

Finite difference step size for HVP (if using 'lanczos_hvp').

None
target_temperature Optional[float]

Target temperature in Kelvin for stability checking. It is strongly recommended to set this explicitly rather than relying on auto-detection from the integrator, which is fragile across different ASE integrator types.

None

Returns:

Type Description
ASEBackend

Fully configured ASEBackend with gades_bias attribute accessible.

Example

from GADES.backend import ASEBackend from GADES.utils import compute_hessian_force_fd_richardson as hessian

backend = ASEBackend.with_gades( ... atoms=atoms, ... base_calc=lammps_calc, ... bias_atom_indices=biasing_atom_ids, ... hess_func=hessian, ... clamp_magnitude=1000, ... kappa=0.9, ... interval=100, ... stability_interval=1000, ... ) backend.integrator = dyn # Attach integrator for step tracking

Source code in GADES/backend.py
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
@classmethod
def with_gades(
    cls,
    atoms: "Atoms",
    base_calc: "Calculator",
    bias_atom_indices: Sequence[int],
    hess_func: Callable,
    clamp_magnitude: float,
    kappa: float,
    interval: int,
    stability_interval: Optional[int] = None,
    logfile_prefix: Optional[str] = None,
    eigensolver: str = "numpy",
    lanczos_iterations: Optional[int] = None,
    use_bofill_update: bool = False,
    full_hessian_interval: Optional[int] = None,
    hvp_epsilon: Optional[float] = None,
    target_temperature: Optional[float] = None,
) -> "ASEBackend":
    """
    Factory method to create an ASEBackend with GADES bias fully configured.

    This method handles all the internal wiring between GADESBias, GADESCalculator,
    and ASEBackend, eliminating the need for manual post-initialization patching.

    Args:
        atoms: The ASE Atoms object.
        base_calc: The base ASE Calculator (e.g., LAMMPS, EMT).
        bias_atom_indices: Indices of atoms that should receive the bias force.
        hess_func: Function to compute the Hessian matrix.
        clamp_magnitude: Maximum magnitude for bias force components.
        kappa: Scaling factor (0 < κ < 1) for the bias force.
        interval: Number of steps between bias force updates.
        stability_interval: Steps between stability checks (optional).
        logfile_prefix: Prefix for log files (optional).
        eigensolver: Method for computing softest eigenmode ('numpy', 'lanczos', or 'lanczos_hvp').
        lanczos_iterations: Number of Lanczos iterations (if using 'lanczos' or 'lanczos_hvp').
        use_bofill_update: Whether to use Bofill Hessian updates.
        full_hessian_interval: Steps between full Hessian recomputation (if using Bofill).
        hvp_epsilon: Finite difference step size for HVP (if using 'lanczos_hvp').
        target_temperature: Target temperature in Kelvin for stability checking.
            **It is strongly recommended to set this explicitly** rather than
            relying on auto-detection from the integrator, which is fragile
            across different ASE integrator types.

    Returns:
        Fully configured ASEBackend with ``gades_bias`` attribute accessible.

    Example:
        >>> from GADES.backend import ASEBackend
        >>> from GADES.utils import compute_hessian_force_fd_richardson as hessian
        >>>
        >>> backend = ASEBackend.with_gades(
        ...     atoms=atoms,
        ...     base_calc=lammps_calc,
        ...     bias_atom_indices=biasing_atom_ids,
        ...     hess_func=hessian,
        ...     clamp_magnitude=1000,
        ...     kappa=0.9,
        ...     interval=100,
        ...     stability_interval=1000,
        ... )
        >>> backend.integrator = dyn  # Attach integrator for step tracking
    """
    # Validate bias_atom_indices against atoms size before creating GADESBias
    # (GADESBias validation is skipped when backend=None)
    n_atoms = len(atoms)
    max_index = max(bias_atom_indices)
    if max_index >= n_atoms:
        raise ValueError(
            f"bias_atom_indices contains index {max_index}, but system only has "
            f"{n_atoms} atoms (valid range: 0 to {n_atoms - 1})"
        )

    # Import here to avoid circular import at module load time
    from .gades import GADESBias

    # Step 1: Create GADESBias with backend=None (will be set later)
    gades_bias = GADESBias(
        backend=None,  # type: ignore[arg-type]
        biased_force=None,
        bias_atom_indices=bias_atom_indices,
        hess_func=hess_func,
        clamp_magnitude=clamp_magnitude,
        kappa=kappa,
        interval=interval,
        stability_interval=stability_interval,
        logfile_prefix=logfile_prefix,
        eigensolver=eigensolver,
        lanczos_iterations=lanczos_iterations,
        use_bofill_update=use_bofill_update,
        full_hessian_interval=full_hessian_interval,
        hvp_epsilon=hvp_epsilon,
    )

    # Step 2: Create GADESCalculator wrapping the base calculator
    gades_calc = GADESCalculator(base_calc, gades_bias)

    # Step 3: Create ASEBackend instance
    backend = cls(gades_calc, atoms, target_temperature=target_temperature)

    # Step 4: Wire up the backend reference in GADESBias
    gades_bias.backend = backend

    # Step 5: Store reference for user access
    backend.gades_bias = gades_bias

    return backend

GADESCalculator

GADESCalculator(base_calc, gades_force_updater)

Bases: Calculator

ASE Calculator wrapper that adds GADES bias forces to an existing ASE Calculator.

Parameters:

Name Type Description Default
base_calc Calculator

The base ASE Calculator to which GADES bias forces will be added.

required
gades_force_updater Any

The GADES force updater object responsible for computing and applying bias forces.

required

Raises:

Type Description
ImportError

If ASE is not installed.

Source code in GADES/backend.py
502
503
504
505
506
507
508
509
510
511
512
def __init__(self, base_calc: "Calculator", gades_force_updater: Any) -> None:
    if not _ASE_AVAILABLE:
        raise ImportError("ASE is required for GADESCalculator. Install with: pip install ase")
    super().__init__()
    self.base_calc = base_calc
    self.force_updater = gades_force_updater
    self.atoms = base_calc.atoms
    self._name = "gades_calculator"
    # Persistent bias state for continuous application between update intervals
    self._stored_bias: Optional[np.ndarray] = None
    self._bias_active: bool = False