Skip to content

Commit 2f22d69

Browse files
author
Aaron Kaplan
committed
fix atomic masses in openff + add optional kwarg for these
1 parent 1cb4a82 commit 2f22d69

File tree

3 files changed

+49
-17
lines changed

3 files changed

+49
-17
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ strict-openff = [
121121
"monty==2025.3.3",
122122
"openmm-mdanalysis-reporter==0.1.0",
123123
"openmm==8.1.1",
124-
"pymatgen==2024.11.13", # DO NOT CHANGE FROM 2024.11.13 - open ff is extremely sensitive to pymatgen version
124+
"pymatgen==2025.6.14", # EXERCISE CAUTION WHEN UPDATING - open ff is extremely sensitive to pymatgen version
125125
]
126126
strict-forcefields = [
127127
"calorine==3.1",

src/atomate2/openff/utils.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from emmet.core.openff import MoleculeSpec
1212
from pymatgen.core import Element, Molecule
1313
from pymatgen.io.openff import create_openff_mol
14+
from scipy.units import Avogadro
1415

1516
if TYPE_CHECKING:
1617
import pathlib
@@ -145,6 +146,7 @@ def calculate_elyte_composition(
145146
salts: dict[str, float],
146147
solvent_densities: dict = None,
147148
solvent_ratio_dimension: Literal["mass", "volume"] = "mass",
149+
atomic_masses: dict[int, float] | None = None,
148150
) -> dict[str, float]:
149151
"""Calculate the normalized mass ratios of an electrolyte solution.
150152
@@ -158,6 +160,9 @@ def calculate_elyte_composition(
158160
Dictionary of solvent SMILES strings and their densities (g/ml).
159161
solvent_ratio_dimension: optional, str
160162
Whether the solvents are included with a ratio of "mass" or "volume"
163+
atomic_masses : dict[int,float] or None (Default)
164+
The mass of each element in the species dict. Defaults to the
165+
most recent data from pymatgen.core.periodic_table.Element
161166
162167
Returns
163168
-------
@@ -186,11 +191,11 @@ def calculate_elyte_composition(
186191
}
187192

188193
# Calculate the molecular weights of the solvent
189-
masses = {el.Z: el.atomic_mass for el in Element}
194+
atomic_masses = atomic_masses or {el.Z: el.atomic_mass for el in Element}
190195
salt_mws = {}
191196
for smile in salts:
192197
mol = tk.Molecule.from_smiles(smile, allow_undefined_stereo=True)
193-
salt_mws[smile] = sum(masses[atom.atomic_number] for atom in mol.atoms)
198+
salt_mws[smile] = sum(atomic_masses[atom.atomic_number] for atom in mol.atoms)
194199

195200
# Convert salt mole ratios to mass ratios
196201
salt_mass_ratio = {
@@ -207,7 +212,9 @@ def calculate_elyte_composition(
207212
return {species: mass / total_mass for species, mass in combined_mass_ratio.items()}
208213

209214

210-
def counts_from_masses(species: dict[str, float], n_mol: int) -> dict[str, float]:
215+
def counts_from_masses(
216+
species: dict[str, float], n_mol: int, atomic_masses: dict[int, float] | None = None
217+
) -> dict[str, float]:
211218
"""Calculate the number of mols needed to yield a given mass ratio.
212219
213220
Parameters
@@ -216,19 +223,21 @@ def counts_from_masses(species: dict[str, float], n_mol: int) -> dict[str, float
216223
Dictionary of species SMILES strings and their relative mass fractions.
217224
n_mol : float
218225
Total number of mols. Returned array will sum to near n_mol.
219-
226+
atomic_masses : dict[int,float] or None (Default)
227+
The mass of each element in the species dict. Defaults to the
228+
most recent data from pymatgen.core.periodic_table.Element
220229
221230
Returns
222231
-------
223232
numpy.ndarray
224233
n_mols: Number of each SMILES needed for the given mass ratio.
225234
"""
226-
masses = {el.Z: el.atomic_mass for el in Element}
235+
atomic_masses = atomic_masses or {el.Z: el.atomic_mass for el in Element}
227236

228237
mol_weights = []
229238
for smile in species:
230239
mol = tk.Molecule.from_smiles(smile, allow_undefined_stereo=True)
231-
mol_weights.append(sum(masses[atom.atomic_number] for atom in mol.atoms))
240+
mol_weights.append(sum(atomic_masses[atom.atomic_number] for atom in mol.atoms))
232241

233242
mol_ratio = np.array(list(species.values())) / np.array(mol_weights)
234243
mol_ratio /= sum(mol_ratio)
@@ -239,7 +248,10 @@ def counts_from_masses(species: dict[str, float], n_mol: int) -> dict[str, float
239248

240249

241250
def counts_from_box_size(
242-
species: dict[str, float], side_length: float, density: float = 0.8
251+
species: dict[str, float],
252+
side_length: float,
253+
density: float = 0.8,
254+
atomic_masses: dict[int, float] | None = None,
243255
) -> dict[str, float]:
244256
"""Calculate the number of molecules needed to fill a box.
245257
@@ -251,25 +263,27 @@ def counts_from_box_size(
251263
Side length of the cubic simulation box in nm.
252264
density : int, optional
253265
Density of the system in g/cm^3. Default is 1 g/cm^3.
266+
atomic_masses : dict[int,float] or None (Default)
267+
The mass of each element in the species dict. Defaults to the
268+
most recent data from pymatgen.core.periodic_table.Element
254269
255270
Returns
256271
-------
257272
dict of str, float
258273
Number of each species needed to fill the box with the given density.
259274
"""
260-
masses = {el.Z: el.atomic_mass for el in Element}
275+
atomic_masses = atomic_masses or {el.Z: el.atomic_mass for el in Element}
261276

262-
na = 6.02214076e23 # Avogadro's number
263277
volume = (side_length * 1e-7) ** 3 # Convert from nm3 to cm^3
264278
total_mass = volume * density # grams
265279

266280
# Calculate molecular weights
267281
mol_weights = []
268282
for smile in species:
269283
mol = tk.Molecule.from_smiles(smile, allow_undefined_stereo=True)
270-
mol_weights.append(sum(masses[atom.atomic_number] for atom in mol.atoms))
284+
mol_weights.append(sum(atomic_masses[atom.atomic_number] for atom in mol.atoms))
271285
mean_mw = np.mean(mol_weights)
272-
n_mol = (total_mass / mean_mw) * na
286+
n_mol = (total_mass / mean_mw) * Avogadro
273287

274288
# Calculate the number of moles needed for each species
275289
mol_ratio = np.array(list(species.values())) / np.array(mol_weights)

tests/openff_md/test_utils.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@
2626
mol_graph_to_openff_mol,
2727
)
2828

29+
# Ensure consistency of test results by fixing atomic masses (no updates)
30+
LEGACY_ATOMIC_MASSES = {
31+
1: 1.00794,
32+
3: 6.941,
33+
6: 12.0107,
34+
8: 15.9994,
35+
9: 18.9984032,
36+
15: 30.973762,
37+
}
38+
2939

3040
def test_molgraph_to_openff_pf6(mol_files):
3141
"""transform a water MoleculeGraph to a OpenFF water molecule"""
@@ -229,9 +239,13 @@ def test_calculate_elyte_composition():
229239
solvent_densities = {"O": 1.0, "CCO": 0.8}
230240

231241
comp_dict = calculate_elyte_composition(
232-
vol_ratio, salts, solvent_densities, "volume"
242+
vol_ratio,
243+
salts,
244+
solvent_densities,
245+
"volume",
246+
atomic_masses=LEGACY_ATOMIC_MASSES,
233247
)
234-
counts = counts_from_masses(comp_dict, 100)
248+
counts = counts_from_masses(comp_dict, 100, atomic_masses=LEGACY_ATOMIC_MASSES)
235249
assert sum(counts.values()) == 100
236250

237251
mol_ratio = {
@@ -242,15 +256,19 @@ def test_calculate_elyte_composition():
242256
"CC#N": 0.130,
243257
"FC1COC(=O)O1": 0.028,
244258
}
245-
counts2 = counts_from_masses(mol_ratio, 1000)
259+
counts2 = counts_from_masses(mol_ratio, 1000, atomic_masses=LEGACY_ATOMIC_MASSES)
246260
assert np.allclose(sum(counts2.values()), 1000, atol=5)
247261

248262

249263
def test_counts_calculators():
250264
mass_fractions = {"O": 0.5, "CCO": 0.5}
251265

252-
counts_size = counts_from_box_size(mass_fractions, 3)
253-
counts_number = counts_from_masses(mass_fractions, 324)
266+
counts_size = counts_from_box_size(
267+
mass_fractions, 3, atomic_masses=LEGACY_ATOMIC_MASSES
268+
)
269+
counts_number = counts_from_masses(
270+
mass_fractions, 324, atomic_masses=LEGACY_ATOMIC_MASSES
271+
)
254272

255273
assert 200 < sum(counts_size.values()) < 500
256274

0 commit comments

Comments
 (0)