Skip to content

Commit fcefdf0

Browse files
committed
Merge branch 'openff-support' into v2.0
Adds the optional OpenFF (SMIRNOFF + NAGL) force field backend.
2 parents 5cf70f2 + d26ad7d commit fcefdf0

20 files changed

Lines changed: 2160 additions & 27 deletions

‎AutoPoly/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"get_strategy",
7171
"Reactor",
7272
"ReactorResult",
73+
"OpenFFTyper",
7374
"mc",
7475
"__version__",
7576
"__author__",
@@ -115,6 +116,10 @@
115116
)
116117
from .reactor import Reactor, ReactorResult
117118
from . import mc
119+
# OpenFFTyper imports the openff packages lazily (only when used), so the
120+
# class itself is always importable; parameterization without the optional
121+
# openff-toolkit installed raises GenerationError with install hints.
122+
from .forcefields.openff_typing import OpenFFTyper
118123
except ImportError as e:
119124
import warnings
120125
warnings.warn(
@@ -128,4 +133,5 @@
128133
generate = None
129134
Reactor = None
130135
ReactorResult = None
136+
OpenFFTyper = None
131137
mc = None

‎AutoPoly/core/conf.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"gaff2": "GAFF2. Updated GAFF with improved parameters.",
4444
"dreiding": "DREIDING. Generic, element-based. Good for unusual chemistry.",
4545
"compass": "COMPASS. Optimized for condensed-phase properties.",
46+
"openff": "OpenFF SMIRNOFF (Sage). Direct chemical perception; NAGL GNN "
47+
"charges by default. Optional: requires openff-toolkit.",
4648
}
4749

4850
# Central registry of per-force-field file/class mappings.
@@ -79,6 +81,15 @@
7981
"inherits": "COMPASS",
8082
"fdefn": "compass_lt.fdefn",
8183
},
84+
# OpenFF is different in kind: no static .lt ships in extern/ and no
85+
# .fdefn SMARTS table is used. Typing runs through the openff-toolkit
86+
# (label_molecules) in pipeline/typing.py, which *generates* a per-system
87+
# openff.lt into the build directory (see forcefields/openff_typing.py).
88+
"openff": {
89+
"lt_file": "openff.lt",
90+
"inherits": "OPENFF",
91+
"fdefn": None,
92+
},
8293
}
8394

8495
VALID_FORCE_FIELDS = list(FORCE_FIELD_REGISTRY.keys())

‎AutoPoly/forcefields/force_field.py‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ class ForceFieldManager:
5656
path_cwd (str): Current working directory for the project
5757
path_master (str): Path to external dependencies
5858
path_moltemplatesrc (str): Path to Moltemplate source
59-
force_field (str): Force field type ("oplsaa", "gaff", "gaff2", or "lopls")
59+
force_field (str): Force field type ("oplsaa", "gaff", "gaff2", "lopls",
60+
"dreiding", "compass", or "openff")
6061
ff_modify_dihedral (np.ndarray): Modified dihedral parameters
6162
path_oplsaaprm (str): Path to OPLS-AA force field parameters
6263
gaff_analyzer (GAFFAnalyzer): GAFF analyzer instance (for GAFF/GAFF2 force field)
@@ -87,12 +88,20 @@ def __init__(self, path_cwd: str, path_master: str, path_moltemplatesrc: str,
8788

8889
# Set force field parameter path based on force_field type
8990
# All force fields now use .lt files from moltemplate/force_fields/
91+
# (openff excepted: its per-system openff.lt is generated at typing
92+
# time, so no bundled file exists under force_fields/)
9093
self.path_oplsaaprm = str(
9194
Path(self.path_master) / "moltemplate" / "force_fields"
9295
/ FORCE_FIELD_REGISTRY[force_field]["lt_file"]
9396
)
9497

95-
logger.info(f"\n'you are now using parameter set of {self.path_oplsaaprm}\n")
98+
if force_field == "openff":
99+
logger.info(
100+
"\n'you are now using a generated SMIRNOFF parameter set "
101+
"(openff.lt from the typing stage)\n"
102+
)
103+
else:
104+
logger.info(f"\n'you are now using parameter set of {self.path_oplsaaprm}\n")
96105

97106
# Initialize GAFF analyzer if using GAFF/GAFF2 force field
98107
self.gaff_analyzer = None
@@ -123,6 +132,8 @@ def make_force_field_lt(self) -> None:
123132
124133
This is the main entry point for force field file generation. It delegates
125134
to the appropriate method based on the force field type:
135+
- For OPENFF: verifies the per-system openff.lt generated by UnitTyper
136+
is in place (nothing to subset — SMIRNOFF parameters are per-molecule)
126137
- For GAFF/GAFF2: creates a filtered subset using GAFFAnalyzer
127138
- For DREIDING/COMPASS: creates a filtered subset based on monomer topology
128139
- For OPLS-AA/LOPLS: copies .lt file directly (new moltemplate 2.22.5 format)
@@ -135,7 +146,9 @@ def make_force_field_lt(self) -> None:
135146
SystemExit: If force field file generation fails
136147
"""
137148
try:
138-
if self.force_field in ("gaff", "gaff2"):
149+
if self.force_field == "openff":
150+
self.make_openff_lt()
151+
elif self.force_field in ("gaff", "gaff2"):
139152
self.make_gaff_lt()
140153
elif self.force_field == "dreiding":
141154
# DREIDING: create subset based on topology
@@ -150,6 +163,34 @@ def make_force_field_lt(self) -> None:
150163
except Exception as e:
151164
raise WorkflowError(f"Error in make_force_field_lt: {str(e)}") from e
152165

166+
def make_openff_lt(self) -> None:
167+
"""
168+
Verify the generated openff.lt is in place (OpenFF path).
169+
170+
Unlike the static force fields, openff.lt is not a bundled parameter
171+
file: SMIRNOFF assigns parameters per molecule, so the file is
172+
generated by UnitTyper (stage 2) into the build directory and copied
173+
here (the moltemplate working dir) by BoxPacker via the monomer_files
174+
manifest. There is nothing to subset — this method only checks the
175+
hand-off happened.
176+
177+
Raises:
178+
GenerationError: If openff.lt is missing (e.g. the build dir was
179+
not produced by UnitTyper(force_field='openff')).
180+
"""
181+
dst = Path(self.path_cwd) / "openff.lt"
182+
if not dst.exists():
183+
raise GenerationError(
184+
f"openff.lt not found in {self.path_cwd}. With "
185+
"force_field='openff' the parameter file is generated during "
186+
"typing (UnitTyper) and copied by BoxPacker — do not run the "
187+
"force-field stage from a non-OpenFF build directory."
188+
)
189+
logger.info(
190+
f" Using generated OpenFF parameter file: {dst} "
191+
f"({dst.stat().st_size} bytes)"
192+
)
193+
153194
def make_gaff_lt(self) -> None:
154195
"""
155196
Create GAFF force field file using GAFFAnalyzer.

0 commit comments

Comments
 (0)