"""mBuild packing module: a wrapper for PACKMOL.
http://leandro.iqm.unicamp.br/m3g/packmol/home.shtml
"""
import os
import sys
import tempfile
import warnings
from distutils.spawn import find_executable
from itertools import zip_longest
from subprocess import PIPE, Popen
import numpy as np
from mbuild import clone
from mbuild.box import Box
from mbuild.compound import Compound
from mbuild.exceptions import MBuildError
__all__ = ["fill_box", "fill_region", "fill_sphere", "solvate"]
PACKMOL = find_executable("packmol")
PACKMOL_HEADER = """
tolerance {0:.16f}
filetype xyz
output {1}
seed {2}
sidemax {3}
"""
PACKMOL_SOLUTE = """
structure {0}
number 1
center
fixed {1:.3f} {2:.3f} {3:.3f} 0. 0. 0.
end structure
"""
PACKMOL_BOX = """
structure {0}
number {1:d}
inside box {2:.3f} {3:.3f} {4:.3f} {5:.3f} {6:.3f} {7:.3f}
{8}
end structure
"""
PACKMOL_SPHERE = """
structure {0}
number {1:d}
inside sphere {2:.3f} {3:.3f} {4:.3f} {5:.3f}
{6}
end structure
"""
PACKMOL_CONSTRAIN = """
constrain_rotation x 0. 0.
constrain_rotation y 0. 0.
constrain_rotation z 0. 0.
"""
[docs]def fill_box(
compound,
n_compounds=None,
box=None,
density=None,
overlap=0.2,
seed=12345,
sidemax=100.0,
edge=0.2,
compound_ratio=None,
aspect_ratio=None,
fix_orientation=False,
temp_file=None,
update_port_locations=False,
):
"""Fill a box with an `mbuild.compound` or `Compound` s using PACKMOL.
`fill_box` takes a single `Compound` or a list of `Compound` s and
returns a `Compound` that has been filled to specification by PACKMOL.
When filling a system, two arguments of `n_compounds` , `box` , and
`density` must be specified.
If `n_compounds` and `box` are not None, the specified number of
compounds will be inserted into a box of the specified size.
If `n_compounds` and `density` are not None, the corresponding box size
will be calculated internally. In this case, `n_compounds` must be an int
and not a list of int.
If `box` and `density` are not None, the corresponding number of
compounds will be calculated internally.
For the cases in which `box` is not specified but generated internally,
the default behavior is to calculate a cubic box. Optionally,
`aspect_ratio` can be passed to generate a non-cubic box.
Parameters
----------
compound : mb.Compound or list of mb.Compound
Compound or list of compounds to fill in box.
n_compounds : int or list of int
Number of compounds to be filled in box.
box : mb.Box
Box to be filled by compounds.
density : float, units :math:`kg/m^3` , default=None
Target density for the system in macroscale units. If not None, one of
`n_compounds` or `box` , but not both, must be specified.
overlap : float, units nm, default=0.2
Minimum separation between atoms of different molecules.
seed : int, default=12345
Random seed to be passed to PACKMOL.
sidemax : float, optional, default=100.0
Needed to build an initial approximation of the molecule distribution in
PACKMOL. All system coordinates must fit with in +/- sidemax, so
increase sidemax accordingly to your final box size.
edge : float, units nm, default=0.2
Buffer at the edge of the box to not place molecules. This is necessary
in some systems because PACKMOL does not account for periodic boundary
conditions in its optimization.
compound_ratio : list, default=None
Ratio of number of each compound to be put in box. Only used in the case
of `density` and `box` having been specified, `n_compounds` not
specified, and more than one `compound` .
aspect_ratio : list of float
If a non-cubic box is desired, the ratio of box lengths in the x, y, and
z directions.
fix_orientation : bool or list of bools
Specify that compounds should not be rotated when filling the box,
default=False.
temp_file : str, default=None
File name to write PACKMOL raw output to.
update_port_locations : bool, default=False
After packing, port locations can be updated, but since compounds can be
rotated, port orientation may be incorrect.
Returns
-------
filled : mb.Compound
"""
# check that the user has the PACKMOL binary on their PATH
_check_packmol(PACKMOL)
arg_count = 3 - [n_compounds, box, density].count(None)
if arg_count != 2:
raise ValueError(
"Exactly 2 of `n_compounds`, `box`, and `density` must be "
"specified. {} were given.".format(arg_count)
)
if box is not None:
(box, my_mins, my_maxs) = _validate_box(box)
if not isinstance(compound, (list, set)):
compound = [compound]
if n_compounds is not None and not isinstance(n_compounds, (list, set)):
n_compounds = [n_compounds]
if not isinstance(fix_orientation, (list, set)):
fix_orientation = [fix_orientation] * len(compound)
if compound is not None and n_compounds is not None:
if len(compound) != len(n_compounds):
raise ValueError(
"`compound` and `n_compounds` must be of equal length."
)
if compound is not None:
if len(compound) != len(fix_orientation):
raise ValueError(
"`compound`, `n_compounds`, and `fix_orientation` must be of "
"equal length."
)
if density is not None:
total_mass = _validate_mass(compound, n_compounds)
if box is None and n_compounds is not None:
# Conversion from (amu/(kg/m^3))**(1/3) to nm
L = (total_mass / density) ** (1 / 3) * 1.1841763
if aspect_ratio is None:
(box, my_mins, my_maxs) = _validate_box(
Box(lengths=[L, L, L], angles=[90.0, 90.0, 90.0])
)
else:
L *= np.prod(aspect_ratio) ** (-1 / 3)
(box, my_mins, my_maxs) = _validate_box(
[val * L for val in aspect_ratio]
)
if n_compounds is None and box is not None:
if len(compound) == 1:
# Conversion from kg/m^3 / amu * nm^3 to dimensionless units
n_compounds = [
int(
density
/ total_mass
* np.prod(np.asarray(box.lengths))
* 0.60224
)
]
else:
if compound_ratio is None:
raise ValueError(
"Determing `n_compounds` from `density` and `box` for "
"systems with more than one compound type requires "
"`compound_ratio`"
)
if len(compound) != len(compound_ratio):
raise ValueError(
"Length of `compound_ratio` must equal length of "
"`compound`"
)
prototype_mass = 0
for c, r in zip(compound, compound_ratio):
prototype_mass += r * c.mass
# Conversion from kg/m^3 / amu * nm^3 to dimensionless units
n_prototypes = int(
density
/ prototype_mass
* np.prod(np.asarray(box.lengths))
* 0.60224
)
n_compounds = list()
for c in compound_ratio:
n_compounds.append(int(n_prototypes * c))
# Convert nm to angstroms for PACKMOL.
# TODO how to handle box starts and ends
# not really a thing a box would know?
box_mins = np.asarray(my_mins) * 10
box_maxs = np.asarray(my_maxs) * 10
overlap *= 10
# Apply 0.2nm edge buffer
box_maxs = [a_max - (edge * 10) for a_max in box_maxs]
box_mins = [a_min + (edge * 10) for a_min in box_mins]
# Build the input file for each compound and call packmol.
filled_xyz = _new_xyz_file()
# create a list to contain the file handles for the compound temp files
compound_xyz_list = list()
try:
input_text = PACKMOL_HEADER.format(
overlap, filled_xyz.name, seed, sidemax * 10
)
for comp, m_compounds, rotate in zip(
compound, n_compounds, fix_orientation
):
m_compounds = int(m_compounds)
compound_xyz = _new_xyz_file()
compound_xyz_list.append(compound_xyz)
comp.save(compound_xyz.name, overwrite=True)
input_text += PACKMOL_BOX.format(
compound_xyz.name,
m_compounds,
box_mins[0],
box_mins[1],
box_mins[2],
box_maxs[0],
box_maxs[1],
box_maxs[2],
PACKMOL_CONSTRAIN if rotate else "",
)
_run_packmol(input_text, filled_xyz, temp_file)
# Create the topology and update the coordinates.
filled = Compound()
filled = _create_topology(filled, compound, n_compounds)
filled.update_coordinates(
filled_xyz.name, update_port_locations=update_port_locations
)
filled.box = box
# ensure that the temporary files are removed from the machine after filling
finally:
for file_handle in compound_xyz_list:
file_handle.close()
os.unlink(file_handle.name)
filled_xyz.close()
os.unlink(filled_xyz.name)
return filled
[docs]def fill_region(
compound,
n_compounds,
region,
overlap=0.2,
bounds=None,
seed=12345,
sidemax=100.0,
edge=0.2,
fix_orientation=False,
temp_file=None,
update_port_locations=False,
):
"""Fill a region of a box with `mbuild.Compound` (s) using PACKMOL.
Parameters
----------
compound : mb.Compound or list of mb.Compound
Compound or list of compounds to fill in region.
n_compounds : int or list of ints
Number of compounds to be put in region.
region : mb.Box or list of mb.Box
Region to be filled by compounds.
overlap : float, units nm, default=0.2
Minimum separation between atoms of different molecules.
seed : int, default=12345
Random seed to be passed to PACKMOL.
sidemax : float, optional, default=100.0
Needed to build an initial approximation of the molecule distribution in
PACKMOL. All system coordinates must fit with in +/- sidemax, so
increase sidemax accordingly to your final box size.
edge : float, units nm, default=0.2
Buffer at the edge of the region to not place molecules. This is
necessary in some systems because PACKMOL does not account for periodic
boundary conditions in its optimization.
fix_orientation : bool or list of bools
Specify that compounds should not be rotated when filling the box,
default=False.
bounds : list-like of floats [minx, miny, minz, maxx, maxy, maxz], units nm, default=None
Bounding within box to pack compounds, if you want to pack within a bounding
area that is not the full extent of the region, bounds are required.
temp_file : str, default=None
File name to write PACKMOL raw output to.
update_port_locations : bool, default=False
After packing, port locations can be updated, but since compounds can be
rotated, port orientation may be incorrect.
Returns
-------
filled : mb.Compound
If using mulitple regions and compounds, the nth value in each list are used
in order.
For example, the third compound will be put in the third region using the
third value in n_compounds.
"""
# check that the user has the PACKMOL binary on their PATH
_check_packmol(PACKMOL)
if not isinstance(compound, (list, set)):
compound = [compound]
if not isinstance(n_compounds, (list, set)):
n_compounds = [n_compounds]
if not isinstance(fix_orientation, (list, set)):
fix_orientation = [fix_orientation] * len(compound)
if compound is not None and n_compounds is not None:
if len(compound) != len(n_compounds):
raise ValueError(
"`compound` and `n_compounds` must be of equal length."
)
if compound is not None:
if len(compound) != len(fix_orientation):
raise ValueError(
"`compound`, `n_compounds`, and `fix_orientation` must be of "
"equal length."
)
# See if region is a single region or list
my_regions = []
if isinstance(region, Box): # Cannot iterate over boxes
my_regions.append(region)
# if region is a list of boxes or a list of lists of floats append to my_regions, otherwise the list is expected to be a list of floats
elif isinstance(region, list):
for reg in region:
if isinstance(reg, (list, Box)):
my_regions.append(reg)
else:
raise ValueError(
f"list contents expected to be mbuild.Box or list of floats, provided: {type(reg)}"
)
else:
raise ValueError(
f"expected a list of type: list or mbuild.Box, was provided {region} of type: {type(region)}"
)
container = []
if not bounds:
bounds = []
for bound, reg in zip_longest(bounds, my_regions, fillvalue=None):
if bound is None:
container.append(reg)
else:
container.append(bound)
container = [_validate_box(bounding) for bounding in container]
# In angstroms for packmol.
overlap *= 10
# Build the input file and call packmol.
filled_xyz = _new_xyz_file()
# List to hold file handles for the temporary compounds
compound_xyz_list = list()
try:
input_text = PACKMOL_HEADER.format(
overlap, filled_xyz.name, seed, sidemax * 10
)
for comp, m_compounds, rotate, items_n in zip(
compound, n_compounds, fix_orientation, container
):
m_compounds = int(m_compounds)
compound_xyz = _new_xyz_file()
compound_xyz_list.append(compound_xyz)
comp.save(compound_xyz.name, overwrite=True)
# TODO how to handle these mins and maxs of this system
# box should not have any idea of mins and maxs
my_min = items_n[1]
my_max = items_n[2]
reg_mins = np.asarray(my_min) * 10.0
reg_maxs = np.asarray(my_max) * 10.0
reg_maxs -= edge * 10 # Apply edge buffer
input_text += PACKMOL_BOX.format(
compound_xyz.name,
m_compounds,
reg_mins[0],
reg_mins[1],
reg_mins[2],
reg_maxs[0],
reg_maxs[1],
reg_maxs[2],
PACKMOL_CONSTRAIN if rotate else "",
)
_run_packmol(input_text, filled_xyz, temp_file)
# Create the topology and update the coordinates.
filled = Compound()
filled = _create_topology(filled, compound, n_compounds)
filled.update_coordinates(
filled_xyz.name, update_port_locations=update_port_locations
)
finally:
for file_handle in compound_xyz_list:
file_handle.close()
os.unlink(file_handle.name)
filled_xyz.close()
os.unlink(filled_xyz.name)
return filled
[docs]def fill_sphere(
compound,
sphere,
n_compounds=None,
density=None,
overlap=0.2,
seed=12345,
sidemax=100.0,
edge=0.2,
compound_ratio=None,
fix_orientation=False,
temp_file=None,
update_port_locations=False,
):
"""Fill a sphere with a compound using PACKMOL.
One argument of `n_compounds and density` must be specified.
If `n_compounds` is not None, the specified number of n_compounds will be
inserted into a sphere of the specified size.
If `density` is not None, the corresponding number of compounds will be
calculated internally.
Parameters
----------
compound : mb.Compound or list of mb.Compound
Compound or list of compounds to be put in box.
sphere : list, units nm
Sphere coordinates in the form [x_center, y_center, z_center, radius]
n_compounds : int or list of int
Number of compounds to be put in box.
density : float, units :math:`kg/m^3`, default=None
Target density for the sphere in macroscale units.
overlap : float, units nm, default=0.2
Minimum separation between atoms of different molecules.
seed : int, default=12345
Random seed to be passed to PACKMOL.
sidemax : float, optional, default=100.0
Needed to build an initial approximation of the molecule distribution in
PACKMOL. All system coordinates must fit with in +/- sidemax, so
increase sidemax accordingly to your final sphere size
edge : float, units nm, default=0.2
Buffer at the edge of the sphere to not place molecules. This is
necessary in some systems because PACKMOL does not account for periodic
boundary conditions in its optimization.
compound_ratio : list, default=None
Ratio of number of each compound to be put in sphere. Only used in the
case of `density` having been specified, `n_compounds` not specified,
and more than one `compound`.
fix_orientation : bool or list of bools
Specify that compounds should not be rotated when filling the sphere,
default=False.
temp_file : str, default=None
File name to write PACKMOL raw output to.
update_port_locations : bool, default=False
After packing, port locations can be updated, but since compounds can be
rotated, port orientation may be incorrect.
Returns
-------
filled : mb.Compound
"""
_check_packmol(PACKMOL)
arg_count = 2 - [n_compounds, density].count(None)
if arg_count != 1:
raise ValueError(
"Exactly 1 of `n_compounds` and `density` must be specified. "
"{} were given.".format(arg_count)
)
if isinstance(sphere, (list, set, tuple)):
if len(sphere) != 4:
raise ValueError("`sphere` must be a list of len 4")
else:
raise TypeError("`sphere` must be a list")
if not isinstance(compound, (list, set)):
compound = [compound]
if n_compounds is not None and not isinstance(n_compounds, (list, set)):
n_compounds = [n_compounds]
if not isinstance(fix_orientation, (list, set)):
fix_orientation = [fix_orientation] * len(compound)
if compound is not None and n_compounds is not None:
if len(compound) != len(n_compounds):
raise ValueError(
"`compound` and `n_compounds` must be of equal length."
)
if compound is not None:
if len(compound) != len(fix_orientation):
raise ValueError(
"`compound`, `n_compounds`, and `fix_orientation` must be of "
"equal length."
)
for coord in sphere[:3]:
if coord < sphere[3]:
raise ValueError(
"`sphere` center coordinates must be greater than radius."
)
# Apply edge buffer
radius = sphere[3] - edge
if density is not None:
total_mass = _validate_mass(compound, n_compounds)
if n_compounds is None:
if len(compound) == 1:
# Conversion from kg/m^3 / amu * nm^3 to dimensionless units
n_compounds = [
int(
density
/ total_mass
* (4 / 3 * np.pi * radius ** 3)
* 0.60224
)
]
else:
if compound_ratio is None:
raise ValueError(
"Determing `n_compounds` from `density` for systems "
"with more than one compound type requires"
"`compound_ratio`"
)
if len(compound) != len(compound_ratio):
raise ValueError(
"Length of `compound_ratio` must equal length of "
"`compound`"
)
prototype_mass = 0
for c, r in zip(compound, compound_ratio):
prototype_mass += r * c.mass
# Conversion from kg/m^3 / amu * nm^3 to dimensionless units
n_prototypes = int(
density
/ prototype_mass
* (4 / 3 * np.pi * radius ** 3)
* 0.60224
)
n_compounds = list()
for c in compound_ratio:
n_compounds.append(int(n_prototypes * c))
# In angstroms for packmol.
sphere = np.multiply(sphere, 10)
radius *= 10
overlap *= 10
# Build the input file for each compound and call packmol.
filled_xyz = _new_xyz_file()
# List to hold file handles for the temporary compounds
compound_xyz_list = list()
try:
input_text = PACKMOL_HEADER.format(
overlap, filled_xyz.name, seed, sidemax * 10
)
for comp, m_compounds, rotate in zip(
compound, n_compounds, fix_orientation
):
m_compounds = int(m_compounds)
compound_xyz = _new_xyz_file()
compound_xyz_list.append(compound_xyz)
comp.save(compound_xyz.name, overwrite=True)
input_text += PACKMOL_SPHERE.format(
compound_xyz.name,
m_compounds,
sphere[0],
sphere[1],
sphere[2],
radius,
PACKMOL_CONSTRAIN if rotate else "",
)
_run_packmol(input_text, filled_xyz, temp_file)
# Create the topology and update the coordinates.
filled = Compound()
filled = _create_topology(filled, compound, n_compounds)
filled.update_coordinates(
filled_xyz.name, update_port_locations=update_port_locations
)
finally:
for file_handle in compound_xyz_list:
file_handle.close()
os.unlink(file_handle.name)
filled_xyz.close()
os.unlink(filled_xyz.name)
return filled
[docs]def solvate(
solute,
solvent,
n_solvent,
box,
overlap=0.2,
seed=12345,
sidemax=100.0,
edge=0.2,
fix_orientation=False,
temp_file=None,
update_port_locations=False,
):
"""Solvate a compound in a box of solvent using PACKMOL.
Parameters
----------
solute : mb.Compound
Compound to be placed in a box and solvated.
solvent : mb.Compound
Compound to solvate the box.
n_solvent : int
Number of solvents to be put in box.
box : mb.Box
Box to be filled by compounds.
overlap : float, units nm, default=0.2
Minimum separation between atoms of different molecules.
seed : int, default=12345
Random seed to be passed to PACKMOL.
sidemax : float, optional, default=100.0
Needed to build an initial approximation of the molecule distribution in
PACKMOL. All system coordinates must fit with in +/- sidemax, so
increase sidemax accordingly to your final box size
edge : float, units nm, default=0.2
Buffer at the edge of the box to not place molecules. This is necessary
in some systems because PACKMOL does not account for periodic boundary
conditions in its optimization.
fix_orientation : bool
Specify if solvent should not be rotated when filling box,
default=False.
temp_file : str, default=None
File name to write PACKMOL raw output to.
update_port_locations : bool, default=False
After packing, port locations can be updated, but since compounds can be
rotated, port orientation may be incorrect.
Returns
-------
solvated : mb.Compound
"""
# check that the user has the PACKMOL binary on their PATH
_check_packmol(PACKMOL)
(box, min_tmp, max_tmp) = _validate_box(box)
if not isinstance(solvent, (list, set)):
solvent = [solvent]
if not isinstance(n_solvent, (list, set)):
n_solvent = [n_solvent]
if not isinstance(fix_orientation, (list, set)):
fix_orientation = [fix_orientation] * len(solvent)
if len(solvent) != len(n_solvent):
raise ValueError("`n_solvent` and `n_solvent` must be of equal length.")
# In angstroms for packmol.
box_mins = np.asarray(min_tmp) * 10
box_maxs = np.asarray(max_tmp) * 10
overlap *= 10
center_solute = (box_maxs + box_mins) / 2
# Apply edge buffer
box_maxs = np.subtract(box_maxs, edge * 10)
box_mins = np.add(box_mins, edge * 10)
# Build the input file for each compound and call packmol.
solvated_xyz = _new_xyz_file()
solute_xyz = _new_xyz_file()
# generate list of temp files for the solvents
solvent_xyz_list = list()
try:
solute.save(solute_xyz.name, overwrite=True)
input_text = PACKMOL_HEADER.format(
overlap, solvated_xyz.name, seed, sidemax * 10
) + PACKMOL_SOLUTE.format(solute_xyz.name, *center_solute.tolist())
for (solv, m_solvent, rotate) in zip(
solvent, n_solvent, fix_orientation
):
m_solvent = int(m_solvent)
solvent_xyz = _new_xyz_file()
solvent_xyz_list.append(solvent_xyz)
solv.save(solvent_xyz.name, overwrite=True)
input_text += PACKMOL_BOX.format(
solvent_xyz.name,
m_solvent,
box_mins[0],
box_mins[1],
box_mins[2],
box_maxs[0],
box_maxs[1],
box_maxs[2],
PACKMOL_CONSTRAIN if rotate else "",
)
_run_packmol(input_text, solvated_xyz, temp_file)
# Create the topology and update the coordinates.
solvated = Compound()
solvated.add(solute)
solvated = _create_topology(solvated, solvent, n_solvent)
solvated.update_coordinates(
solvated_xyz.name, update_port_locations=update_port_locations
)
finally:
for file_handle in solvent_xyz_list:
file_handle.close()
os.unlink(file_handle.name)
solvated_xyz.close()
solute_xyz.close()
os.unlink(solvated_xyz.name)
os.unlink(solute_xyz.name)
return solvated
def _validate_mass(compound, n_compounds):
"""Check the mass of the compounds passed into the packing functions.
Raises an error if the total mass is zero, and density cannot be used to
find box size or number of compounds.
Returns a warning of any subcompound in compound has a mass of zero.
"""
if n_compounds is None:
n_compounds = [1] * len(compound)
found_zero_mass = False
total_mass = 0
for c, n in zip(compound, n_compounds):
comp_masses = [c._particle_mass(p) for p in c.particles()]
if 0.0 in comp_masses:
found_zero_mass = True
total_mass += np.sum(comp_masses) * n
if total_mass == 0:
raise MBuildError(
"The total mass of your compound(s) is zero "
"In order to use density when packing a box, the mass of "
"the compounds must be set. See the doc strings of the "
"Compound() class in compound.py for more information "
"on how mass is handled."
)
if found_zero_mass:
warnings.warn(
"Some of the compounds or subcompounds in `compound` "
"have a mass of zero. This may have an effect on "
"density calculations"
)
return total_mass
def _validate_box(box):
"""Ensure that the box passed by the user can be formatted as an mbuild.Box.
Parameters
----------
box : mbuild.Box or a tuple or list thereof
Box or inputs to `mbuild.Box` to generate a `mbuild.Box`.
Returns
-------
box : mbuild.Box
mins : list-like
maxs : list-like
"""
if isinstance(box, (list, tuple)):
if len(box) == 3:
mins = [0.0, 0.0, 0.0]
maxs = box
box = Box.from_mins_maxs_angles(
mins=mins, maxs=maxs, angles=(90.0, 90.0, 90.0)
)
elif len(box) == 6:
mins = box[:3]
maxs = box[3:]
box = Box.from_mins_maxs_angles(
mins=mins, maxs=maxs, angles=(90.0, 90.0, 90.0)
)
else:
raise MBuildError(
"Unknown format for `box` parameter. Must pass a"
" list/tuple of length 3 (box lengths) or length"
" 6 (box mins and maxes) or an mbuild.Box object."
)
elif isinstance(box, Box):
mins = [0.0, 0.0, 0.0]
maxs = box.lengths
else:
raise MBuildError(
"Unknown format for `box` parameter. Must pass a list/tuple of "
"length 3 (box lengths) or length 6 (box mins and maxes) or an "
"mbuild.Box object."
)
return (box, mins, maxs)
def _new_xyz_file():
"""Generate PDB file using tempfile.NamedTemporaryFile.
Return
------
_ : file-object
Temporary PDB file.
"""
return tempfile.NamedTemporaryFile(suffix=".xyz", delete=False)
def _create_topology(container, comp_to_add, n_compounds):
"""Return updated mBuild compound with new coordinates.
Parameters
----------
container : mb.Compound, required
Compound containing the updated system generated by PACKMOL.
comp_to_add : mb.Compound or list of mb.Compounds, required
Compound(s) to add to the container.
container : int or list of int, required
Amount of comp_to_add to container.
Return
------
container : mb.Compound
Compound with added compounds from PACKMOL.
"""
for comp, m_compound in zip(comp_to_add, n_compounds):
for _ in range(m_compound):
container.add(clone(comp))
return container
def _packmol_error(out, err):
"""Log packmol output to files."""
with open("log.txt", "w") as log_file:
log_file.write(out)
raise RuntimeError("PACKMOL failed. See 'log.txt'")
def _run_packmol(input_text, filled_xyz, temp_file):
"""Call PACKMOL to pack system based on the input text.
Parameters
----------
input_text : str, required
String formatted in the input file syntax for PACKMOL.
filled_xyz : `tempfile` object, required
Tempfile that will store the results of PACKMOL packing.
temp_file : `tempfile` object, required
Where to copy the filled tempfile.
"""
# Create input file
packmol_inp = tempfile.NamedTemporaryFile(
mode="w", delete=False, prefix="packmol-", suffix=".inp"
)
packmol_inp.write(input_text)
packmol_inp.close()
proc = Popen(
"{} < {}".format(PACKMOL, packmol_inp.name),
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
universal_newlines=True,
shell=True,
)
out, err = proc.communicate()
if "WITHOUT PERFECT PACKING" in out:
warnings.warn(
"Packmol finished with imperfect packing. Using the .xyz_FORCED "
"file instead. This may not be a sufficient packing result."
)
os.system("cp {0}_FORCED {0}".format(filled_xyz.name))
if "ERROR" in out or proc.returncode != 0:
_packmol_error(out, err)
else:
# Delete input file if success
os.remove(packmol_inp.name)
if temp_file is not None:
os.system("cp {0} {1}".format(filled_xyz.name, os.path.join(temp_file)))
def _check_packmol(PACKMOL): # pragma: no cover
if not PACKMOL:
msg = "Packmol not found."
if sys.platform.startswith("win"):
msg = (
msg + " If packmol is already installed, make sure that the "
"packmol.exe is on the path."
)
raise IOError(msg)