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 """ if isinstance(box, (list, tuple)): if len(box) == 3: box = Box(lengths=box) elif len(box) == 6: box = Box(mins=box[:3], maxs=box[3:]) if not isinstance(box, Box): 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
def from_snapshot(snapshot, scale=1.0): """Convert a Snapshot to a Compound. Snapshot can be a hoomd.data.Snapshot or a gsd.hoomd.Snapshot. Parameters ---------- snapshot : hoomd._hoomd.SnapshotSystemData_float or gsd.hoomd.Snapshot Snapshot from which to build the mbuild Compound. scale : float, optional, default 1.0 Value by which to scale the length values Returns ------- comp : Compound Note ---- GSD and HOOMD snapshots center their boxes on the origin (0,0,0), so the compound is shifted by half the box lengths """ comp = Compound() bond_array = snapshot.bonds.group n_atoms = snapshot.particles.N if "SnapshotSystemData_float" in dir(hoomd._hoomd) and isinstance( snapshot, hoomd._hoomd.SnapshotSystemData_float ): # hoomd v2 box = snapshot.box comp.box = Box.from_lengths_tilt_factors( lengths=np.array([box.Lx, box.Ly, box.Lz]) * scale, tilt_factors=np.array([box.xy, box.xz, box.yz]), ) else: # gsd / hoomd v3 box = snapshot.configuration.box comp.box = Box.from_lengths_tilt_factors( lengths=box[:3] * scale, tilt_factors=box[3:] ) # GSD and HOOMD snapshots center their boxes on the origin (0,0,0) shift = np.array(comp.box.lengths) / 2 # Add particles for i in range(n_atoms): name = snapshot.particles.types[snapshot.particles.typeid[i]] xyz = snapshot.particles.position[i] * scale + shift charge = snapshot.particles.charge[i] atom = Particle(name=name, pos=xyz, charge=charge) comp.add(atom, label=str(i)) # Add bonds particle_dict = {idx: p for idx, p in enumerate(comp.particles())} for i in range(bond_array.shape[0]): atom1 = int(bond_array[i][0]) atom2 = int(bond_array[i][1]) comp.add_bond([particle_dict[atom1], particle_dict[atom2]]) return comp
def _validate_box(box): if isinstance(box, (list, tuple)): if len(box) == 3: box = Box(lengths=box) elif len(box) == 6: box = Box(mins=box[:3], maxs=box[3:]) if not isinstance(box, Box): 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
def test_pass_box(self, mb_ethane): mb_box = Box(lengths=[3, 3, 3]) top = from_mbuild(mb_ethane, box=mb_box) assert_allclose_units(top.box.lengths, [3, 3, 3] * u.nm, rtol=1e-5, atol=1e-8)
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 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 a `mbuild.compound` or `Compound`s using PACKMOL. `fill_box` takes a single `mbuild.Compound` or a list of `mbuild.Compound`'s and return an `mbuild.Compound` that has been filled to the user's specifications to the best of PACKMOL's ability. 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 n_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 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's 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: msg = ("Exactly 2 of `n_compounds`, `box`, and `density` " "must be specified. {} were given.".format(arg_count)) raise ValueError(msg) if box is not None: box = _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): msg = "`compound` and `n_compounds` must be of equal length." raise ValueError(msg) if compound is not None: if len(compound) != len(fix_orientation): msg = ("`compound`, `n_compounds`, and `fix_orientation` " "must be of equal length.") raise ValueError(msg) if density is not None: if box is None and n_compounds is not None: total_mass = np.sum([ n * np.sum([a.mass for a in c.to_parmed().atoms]) for c, n in zip(compound, n_compounds) ]) # Conversion from (amu/(kg/m^3))**(1/3) to nm L = (total_mass / density)**(1 / 3) * 1.1841763 if aspect_ratio is None: box = _validate_box(Box(3 * [L])) else: L *= np.prod(aspect_ratio)**(-1 / 3) box = _validate_box(Box([val * L for val in aspect_ratio])) if n_compounds is None and box is not None: if len(compound) == 1: compound_mass = np.sum( [a.mass for a in compound[0].to_parmed().atoms]) # Conversion from kg/m^3 / amu * nm^3 to dimensionless units n_compounds = [ int(density / compound_mass * np.prod(box.lengths) * 0.60224) ] else: if compound_ratio is None: msg = ("Determing `n_compounds` from `density` and `box` " "for systems with more than one compound type " "requires `compound_ratio`") raise ValueError(msg) if len(compound) != len(compound_ratio): msg = ("Length of `compound_ratio` must equal length of " "`compound`") raise ValueError(msg) prototype_mass = 0 for c, r in zip(compound, compound_ratio): prototype_mass += r * np.sum( [a.mass for a in c.to_parmed().atoms]) # Conversion from kg/m^3 / amu * nm^3 to dimensionless units n_prototypes = int(density / prototype_mass * np.prod(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. box_mins = box.mins * 10 box_maxs = box.maxs * 10 overlap *= 10 # Apply 1nm edge buffer box_maxs -= edge * 10 # 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 filled.periodicity = np.asarray(box.lengths, dtype=np.float32) # 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
def test_pass_box(self, ethane): mb_box = Box(lengths=[3, 3, 3]) top = from_mbuild(ethane, box=mb_box) assert allclose(top.box.lengths, [3, 3, 3] * u.nm)
def fill_box(compound, n_compounds=None, box=None, density=None, overlap=0.2, seed=12345, edge=0.2, compound_ratio=None, aspect_ratio=None, fix_orientation=False, temp_file=None): """Fill a box with a compound using packmol. Two arguments of `n_compounds, box, and density` must be specified. If `n_compounds` and `box` are not None, the specified number of n_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 be put in box. n_compounds : int or list of int Number of compounds to be put in box. box : mb.Box Box to be filled by compounds. density : float, units 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. 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's raw output to. Returns ------- filled : mb.Compound """ _check_packmol(PACKMOL) arg_count = 3 - [n_compounds, box, density].count(None) if arg_count != 2: msg = ("Exactly 2 of `n_compounds`, `box`, and `density` " "must be specified. {} were given.".format(arg_count)) raise ValueError(msg) if box is not None: box = _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): msg = ("`compound` and `n_compounds` must be of equal length.") raise ValueError(msg) if compound is not None: if len(compound) != len(fix_orientation): msg = ("`compound`, `n_compounds`, and `fix_orientation` must be of equal length.") raise ValueError(msg) if density is not None: if box is None and n_compounds is not None: total_mass = np.sum([n*np.sum([a.mass for a in c.to_parmed().atoms]) for c,n in zip(compound, n_compounds)]) # Conversion from (amu/(kg/m^3))**(1/3) to nm L = (total_mass/density)**(1/3)*1.1841763 if aspect_ratio is None: box = _validate_box(Box(3*[L])) else: L *= np.prod(aspect_ratio) ** (-1/3) box = _validate_box(Box([val*L for val in aspect_ratio])) if n_compounds is None and box is not None: if len(compound) == 1: compound_mass = np.sum([a.mass for a in compound[0].to_parmed().atoms]) # Conversion from kg/m^3 / amu * nm^3 to dimensionless units n_compounds = [int(density/compound_mass*np.prod(box.lengths)*.60224)] else: if compound_ratio is None: msg = ("Determing `n_compounds` from `density` and `box` " "for systems with more than one compound type requires" "`compound_ratio`") raise ValueError(msg) if len(compound) != len(compound_ratio): msg = ("Length of `compound_ratio` must equal length of " "`compound`") raise ValueError(msg) prototype_mass = 0 for c, r in zip(compound, compound_ratio): prototype_mass += r * np.sum([a.mass for a in c.to_parmed().atoms]) # Conversion from kg/m^3 / amu * nm^3 to dimensionless units n_prototypes = int(density/prototype_mass*np.prod(box.lengths)*.60224) n_compounds = list() for c in compound_ratio: n_compounds.append(int(n_prototypes * c)) # In angstroms for packmol. box_mins = box.mins * 10 box_maxs = box.maxs * 10 overlap *= 10 # Apply edge buffer box_maxs -= edge * 10 # Build the input file for each compound and call packmol. filled_pdb = tempfile.mkstemp(suffix='.pdb')[1] input_text = PACKMOL_HEADER.format(overlap, filled_pdb, seed) for comp, m_compounds, rotate in zip(compound, n_compounds, fix_orientation): m_compounds = int(m_compounds) compound_pdb = tempfile.mkstemp(suffix='.pdb')[1] comp.save(compound_pdb, overwrite=True) input_text += PACKMOL_BOX.format(compound_pdb, 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_pdb, temp_file) # Create the topology and update the coordinates. filled = Compound() for comp, m_compounds in zip(compound, n_compounds): for _ in range(m_compounds): filled.add(clone(comp)) filled.update_coordinates(filled_pdb) filled.periodicity = np.asarray(box.lengths, dtype=np.float32) return filled
def boundingbox(self): """Compute the bounding box of the compound. """ xyz = self.xyz return Box(mins=xyz.min(axis=0), maxs=xyz.max(axis=0))
def fill_box(compound, n_compounds=None, box=None, density=None, overlap=0.2, seed=12345): """Fill a box with a compound using packmol. Two arguments of `n_compounds, box, and density` must be specified. If `n_compounds` and `box` are not None, the specified number of n_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. Parameters ---------- compound : mb.Compound or list of mb.Compound n_compounds : int or list of int box : mb.Box overlap : float, units nm density : float, units kg/m^3 Returns ------- filled : mb.Compound TODO : Support aspect ratios in generated boxes TODO : Support ratios of n_compounds """ 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) arg_count = 3 - [n_compounds, box, density].count(None) if arg_count != 2: msg = ("Exactly 2 of `n_compounds`, `box`, and `density` " "must be specified. {} were given.".format(arg_count)) raise ValueError(msg) if box is not None: box = _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 density is not None: if box is None and n_compounds is not None: total_mass = np.sum([ n * np.sum([a.mass for a in c.to_parmed().atoms]) for c, n in zip(compound, n_compounds) ]) # Conversion from (amu/(kg/m^3))**(1/3) to nm L = (total_mass / density)**(1 / 3) * 1.1841763 box = _validate_box(Box(3 * [L])) if n_compounds is None and box is not None: if len(compound) > 1: msg = ("Determing `n_compounds` from `density` and `box` " "currently only supported for systems with one " "compound type.") raise ValueError(msg) else: compound_mass = np.sum( [a.mass for a in compound[0].to_parmed().atoms]) # Conversion from kg/m^3 / amu * nm^3 to dimensionless units n_compounds = [ int(density / compound_mass * np.prod(box.lengths) * .60224) ] # In angstroms for packmol. box_mins = box.mins * 10 box_maxs = box.maxs * 10 overlap *= 10 # Build the input file for each compound and call packmol. filled_pdb = tempfile.mkstemp(suffix='.pdb')[1] input_text = PACKMOL_HEADER.format(overlap, filled_pdb, seed) for comp, m_compounds in zip(compound, n_compounds): m_compounds = int(m_compounds) compound_pdb = tempfile.mkstemp(suffix='.pdb')[1] comp.save(compound_pdb, overwrite=True) input_text += PACKMOL_BOX.format(compound_pdb, m_compounds, box_mins[0], box_mins[1], box_mins[2], box_maxs[0], box_maxs[1], box_maxs[2]) proc = Popen(PACKMOL, stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True) out, err = proc.communicate(input=input_text) if err: _packmol_error(out, err) # Create the topology and update the coordinates. filled = Compound() for comp, m_compounds in zip(compound, n_compounds): for _ in range(m_compounds): filled.add(clone(comp)) filled.update_coordinates(filled_pdb) filled.periodicity = np.asarray(box.lengths, dtype=np.float32) return filled