Skip to content

Commit 33ba1c0

Browse files
committed
add easy way to create new substances
1 parent 3b38655 commit 33ba1c0

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

openff/evaluator/_tests/test_substances.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,14 @@ def test_truncate_n_molecules():
9898
def test_substance_len():
9999
substance = Substance.from_components("C", "CC", "CCC", "CCC")
100100
assert len(substance) == 3
101+
102+
103+
def test_to_substance_n_molecules():
104+
substance = Substance()
105+
substance.add_component(Component("C"), MoleFraction(0.509800))
106+
substance.add_component(Component("CC"), MoleFraction(0.490200))
107+
108+
new_substance = substance.to_substance_n_molecules(100)
109+
assert new_substance.number_of_components == 2
110+
assert new_substance.get_amounts(substance.components[0])[0].value == 0.51
111+
assert new_substance.get_amounts(substance.components[1])[0].value == 0.49

openff/evaluator/substances/substances.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,107 @@ def get_amounts(self, component):
219219
identifier = component if isinstance(component, str) else component.identifier
220220

221221
return self.amounts[identifier]
222+
223+
def to_substance_n_molecules(
224+
self,
225+
maximum_molecules: int,
226+
tolerance=None,
227+
count_exact_amount=True,
228+
truncate_n_molecules=True,
229+
) -> "Substance":
230+
"""
231+
Returns a new substance given a maximum total number of molecules.
232+
233+
Parameters
234+
----------
235+
maximum_molecules: int
236+
The maximum number of molecules.
237+
tolerance: float, optional
238+
The tolerance within which this amount should be represented. As
239+
an example, when converting a mole fraction into a number of molecules,
240+
the total number of molecules may not be sufficiently large enough to
241+
reproduce this amount.
242+
count_exact_amount: bool
243+
Whether components present in an exact amount (i.e. defined with an
244+
``ExactAmount``) should be considered when apply the maximum number
245+
of molecules constraint. This may be set false, for example, when
246+
building a separate solvated protein (n = 1) and solvated protein +
247+
ligand complex (n = 2) system but wish for both systems to have the
248+
same number of solvent molecules.
249+
truncate_n_molecules: bool
250+
Whether or not to attempt to truncate the number of molecules in the
251+
substance if the total number is over the specified maximum. If False, an
252+
exception will be raised in this case.
253+
254+
The truncation works by iteratively removing one molecule of the
255+
predominant component up to a limit of removing a total number of molecules
256+
equal to the number of components in the substance (e.g. for a binary
257+
substance a maximum of two molecules can be removed). An exception is
258+
raised if the number of molecules cannot be sensibly truncated.
259+
"""
260+
261+
molecules_per_component = self.get_molecules_per_component(
262+
maximum_molecules,
263+
tolerance=tolerance,
264+
count_exact_amount=count_exact_amount,
265+
truncate_n_molecules=truncate_n_molecules,
266+
)
267+
268+
new_amounts = defaultdict(list)
269+
total_number_of_molecules = sum(molecules_per_component.values())
270+
271+
# Handle any exact amounts.
272+
for component in self.components:
273+
exact_amounts = [
274+
amount
275+
for amount in self.get_amounts(component)
276+
if isinstance(amount, ExactAmount)
277+
]
278+
279+
if len(exact_amounts) == 0:
280+
continue
281+
282+
total_number_of_molecules -= exact_amounts[0].value
283+
new_amounts[component].append(exact_amounts[0])
284+
285+
# Recompute the mole fractions.
286+
total_mole_fraction = 0.0
287+
number_of_new_mole_fractions = 0
288+
289+
for component in self.components:
290+
mole_fractions = [
291+
amount
292+
for amount in self.get_amounts(component)
293+
if isinstance(amount, MoleFraction)
294+
]
295+
296+
if len(mole_fractions) == 0:
297+
continue
298+
299+
molecule_count = molecules_per_component[component.identifier]
300+
301+
if component in new_amounts:
302+
molecule_count -= new_amounts[component][0].value
303+
304+
new_mole_fraction = molecule_count / total_number_of_molecules
305+
new_amounts[component].append(MoleFraction(new_mole_fraction))
306+
307+
total_mole_fraction += new_mole_fraction
308+
number_of_new_mole_fractions += 1
309+
310+
if (
311+
not np.isclose(total_mole_fraction, 1.0)
312+
and number_of_new_mole_fractions > 0
313+
):
314+
raise ValueError("The new mole fraction does not equal 1.0")
315+
316+
output_substance = Substance()
317+
318+
for component, amounts in new_amounts.items():
319+
for amount in amounts:
320+
output_substance.add_component(component, amount)
321+
322+
return output_substance
222323

223324
def get_molecules_per_component(
224325
self,

0 commit comments

Comments
 (0)