Coverage for openxps/couplings/collective_variable_coupling.py: 100%
32 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-13 22:08 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-13 22:08 +0000
1"""
2Collective variable coupling.
4.. module:: openxps.couplings.collective_variable_coupling
5 :platform: Linux, MacOS, Windows
6 :synopsis: Coupling between dynamical variables and physical collective variables
8.. classauthor:: Charlles Abreu <craabreu@gmail.com>
10"""
12import typing as t
14import cvpack
15import openmm as mm
16from openmm import _openmm as mmswig
17from openmm import unit as mmunit
19from openxps.utils import preprocess_args
21from ..dynamical_variable import DynamicalVariable
22from .base import Coupling
25class CollectiveVariableCoupling(Coupling):
26 """Coupling between dynamical variables and physical collective variables.
28 This class uses a :CVPack:`MetaCollectiveVariable` to create a coupling defined by
29 a mathematical expression involving physical collective variables and parameters.
31 Parameters
32 ----------
33 function
34 A mathematical expression that defines the coupling energy as a function of
35 collective variables and parameters.
36 collective_variables
37 The physical collective variables used in the coupling function.
38 dynamical_variables
39 The extended dynamical variables used in the coupling function.
40 **parameters
41 Named parameters that appear in the mathematical expression.
43 Examples
44 --------
45 >>> from copy import copy
46 >>> import cvpack
47 >>> import openxps as xps
48 >>> from openmm import unit
49 >>> from math import pi
50 >>> phi = cvpack.Torsion(6, 8, 14, 16, name="phi")
51 >>> mass = 3 * unit.dalton * (unit.nanometer / unit.radian)**2
52 >>> phi0 = xps.DynamicalVariable("phi0", unit.radian, mass, xps.CircularBounds())
53 >>> xps.CollectiveVariableCoupling(
54 ... f"0.5*kappa*min(delta,{2*pi}-delta)^2; delta=abs(phi-phi0)",
55 ... [phi],
56 ... [phi0],
57 ... kappa=1000 * unit.kilojoules_per_mole / unit.radian**2,
58 ... )
59 CollectiveVariableCoupling("0.5*kappa*min(delta,6.28...-delta)^2; ...")
60 """
62 @preprocess_args
63 def __init__(
64 self,
65 function: str,
66 collective_variables: t.Iterable[cvpack.CollectiveVariable],
67 dynamical_variables: t.Iterable[DynamicalVariable],
68 **parameters: mmunit.Quantity,
69 ) -> None:
70 dv_names = {dv.name for dv in dynamical_variables}
71 filtered_params = {k: v for k, v in parameters.items() if k not in dv_names}
73 force = cvpack.MetaCollectiveVariable(
74 function,
75 collective_variables,
76 unit=mmunit.kilojoule_per_mole,
77 name="coupling",
78 **filtered_params,
79 **{dv.name: 0.0 * dv.unit for dv in dynamical_variables},
80 )
81 super().__init__([force], dynamical_variables)
83 def __repr__(self) -> str:
84 return f'{self.__class__.__name__}("{self._forces[0].getEnergyFunction()}")'
86 def _createFlippedForce(self) -> mm.CustomCVForce:
87 force = self._forces[0]
88 parameters = force.getParameterDefaultValues()
89 dvs_to_flip = [dv for dv in self._dynamical_variables if dv.name in parameters]
90 for dv in dvs_to_flip:
91 parameters.pop(dv.name)
92 parameters.update(
93 {cv.getName(): 0.0 * cv.getUnit() for cv in force.getInnerVariables()}
94 )
95 return cvpack.MetaCollectiveVariable(
96 force.getEnergyFunction(),
97 [
98 dv.createCollectiveVariable(self._dv_indices[dv.name])
99 for dv in dvs_to_flip
100 ],
101 unit=mmunit.kilojoule_per_mole,
102 name="coupling",
103 **parameters,
104 )
106 def updateExtensionContext(
107 self,
108 physical_context: mm.Context,
109 extension_context: mm.Context,
110 ) -> None:
111 force = self._forces[0]
112 collective_variables = mmswig.CustomCVForce_getCollectiveVariableValues(
113 force, physical_context
114 )
115 for index, value in enumerate(collective_variables):
116 name = mmswig.CustomCVForce_getCollectiveVariableName(force, index)
117 mmswig.Context_setParameter(extension_context, name, value)
120CollectiveVariableCoupling.registerTag("!openxps.CollectiveVariableCoupling")