def _perform_sanity_check(json_dict): """Perform Sanity Check on the JSON File.""" from warnings import warn warning_msg = ( "This Json was written using {0}, current mbuild version is {1}.") this_version = mb.__version__ json_mbuild_version = json_dict.get("mbuild-version", None) if not json_mbuild_version: raise MBuildError( "The uploaded JSON file doesn't isn't correctly formatted") json_mb_type = json_dict.get("type", None) if (not json_mb_type) or (json_mb_type != "Compound"): raise MBuildError( "Error. Cannot convert JSON of type: {}".format(json_mb_type)) [major, minor, patch] = json_mbuild_version.split(".") [this_major, this_minor, this_patch] = this_version.split(".") if major != this_major: raise MBuildError( warning_msg.format(json_mbuild_version, this_version) + " Cannot Convert JSON to compound") if minor != this_minor: warn( warning_msg.format(json_mbuild_version, this_version) + " Will Proceed.")
def read_xyz(filename, compound=None): """Read an XYZ file. The expected format is as follows: The first line contains the number of atoms in the file The second line contains a comment, which is not read. Remaining lines, one for each atom in the file, include an elemental symbol followed by X, Y, and Z coordinates in Angstroms. Columns are expected tbe separated by whitespace. See https://openbabel.org/wiki/XYZ_(format). Parameters ---------- filename : str Path of the input file Returns ------- compound : mb.Compound Notes ----- The XYZ file format neglects many important details, notably as bonds, residues, and box information. There are some other flavors of the XYZ file format and not all are guaranteed to be compatible with this reader. For example, the TINKER XYZ format is not expected to be properly read. """ if compound is None: compound = mb.Compound() with open(filename, 'r') as xyz_file: n_atoms = int(xyz_file.readline()) xyz_file.readline() coords = np.zeros(shape=(n_atoms, 3), dtype=np.float64) for row, _ in enumerate(coords): line = xyz_file.readline().split() if not line: msg = ( 'Incorrect number of lines in input file. Based on the ' 'number in the first line of the file, {} rows of atoms ' 'were expected, but at least one fewer was found.') raise MBuildError(msg.format(n_atoms)) coords[row] = line[1:4] coords[row] *= 0.1 particle = mb.Compound(pos=coords[row], name=line[0]) compound.add(particle) # Verify we have read the last line by ensuring the next line in blank line = xyz_file.readline().split() if line: msg = ('Incorrect number of lines in input file. Based on the ' 'number in the first line of the file, {} rows of atoms ' 'were expected, but at least one more was found.') raise MBuildError(msg.format(n_atoms)) return compound
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 """ 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 _normalize_box(vectors): """Align the box matrix into a right-handed coordinate frame. NOTE: This assumes that the matrix is in a row-major format. NOTE: Inspiration and logic are from the Glotzer group package, Garnett; which is provided under a BSD 3-clause License. For additional information, refer to the License file provided with this package. """ det = np.linalg.det(vectors) if np.isclose(det, 0.0, atol=1e-5): raise MBuildError( "The vectors to define the box are co-linear, this does not form a " f"3D region in space.\n Box vectors evaluated: {vectors}") if det < 0.0: warn("Box vectors provided for a left-handed basis, these will be " "transformed into a right-handed basis automatically.") # transpose to column-major for the time being Q, R = np.linalg.qr(vectors.T) # left or right handed: det<0 left, >0, right sign = np.linalg.det(Q) R = R * sign signs = np.diag( np.diag(np.where(R < 0, -np.ones(R.shape), np.ones(R.shape)))) transformed_vecs = R.dot(signs) return _reduced_form_vectors(transformed_vecs.T)
def _clone_bonds(self, clone_of=None): newone = clone_of[self] for c1, c2 in self.bonds(): try: newone.add_bond((clone_of[c1], clone_of[c2])) except KeyError: raise MBuildError( "Cloning failed. Compound contains bonds to " "Particles outside of its containment hierarchy.")
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 _find_particle_image(self, query, match, all_particles): """Find particle with the same index as match in a neighboring tile. """ _, idxs = self.particle_kdtree.query(query.pos, k=10) neighbors = all_particles[idxs] for particle in neighbors: if particle.index == match.index: return particle raise MBuildError('Unable to find matching particle image while' ' stitching bonds.')
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 from_mbuild(cls, compound): """ Instantiates a CG_Compound and follows mb.Compound.deep_copy to copy particles and bonds to CG_Compound Parameters ---------- compound : mb.Compound to be compied Returns ------- CG_Compound """ comp = cls() clone_dict = {} comp.name = deepcopy(compound.name) comp.periodicity = deepcopy(compound.periodicity) comp._pos = deepcopy(compound._pos) comp.port_particle = deepcopy(compound.port_particle) comp._check_if_contains_rigid_bodies = deepcopy( compound._check_if_contains_rigid_bodies ) comp._contains_rigid = deepcopy(compound._contains_rigid) comp._rigid_id = deepcopy(compound._rigid_id) comp._charge = deepcopy(compound._charge) if compound.children is None: comp.children = None else: comp.children = OrderedSet() # Parent should be None initially. comp.parent = None comp.labels = OrderedDict() comp.referrers = set() comp.bond_graph = None for p in compound.particles(): new_particle = mb.Particle(name=p.name, pos=p.xyz.flatten()) comp.add(new_particle) clone_dict[p] = new_particle for c1, c2 in compound.bonds(): try: comp.add_bond((clone_dict[c1], clone_dict[c2])) except KeyError: raise MBuildError( "Cloning failed. Compound contains bonds to " "Particles outside of its containment hierarchy." ) return comp
def _init_hoomd_dihedrals(structure, ref_energy=1.0): """ Periodic dihedrals (dubbed harmonic dihedrals in HOOMD) """ # Identify the unique dihedral types before setting # need Hoomd 2.8.0 to use proper dihedral implemtnation # from this PR https://github.com/glotzerlab/hoomd-blue/pull/492 version_numbers = _check_hoomd_version() if float(version_numbers[0]) < 2 or float(version_numbers[1]) < 8: from mbuild.exceptions import MBuildError raise MBuildError("Please upgrade Hoomd to at least 2.8.0") dihedral_type_params = {} for dihedral in structure.dihedrals: t1, t2 = dihedral.atom1.type, dihedral.atom2.type t3, t4 = dihedral.atom3.type, dihedral.atom4.type if [t2, t3] == sorted([t2, t3], key=natural_sort): dihedral_type = "-".join((t1, t2, t3, t4)) else: dihedral_type = "-".join((t4, t3, t2, t1)) if dihedral_type not in dihedral_type_params: if isinstance(dihedral.type, pmd.DihedralType): dihedral_type_params[dihedral_type] = dihedral.type elif isinstance(dihedral.type, pmd.DihedralTypeList): if len(dihedral.type) > 1: warnings.warn( "Multiple dihedral types detected" + " for single dihedral, will ignore all except " + " first dihedral type." + "First dihedral type: {}".format(dihedral.type[0])) dihedral_type_params[dihedral_type] = dihedral.type[0] # Set the hoomd parameters # These are periodic torsions periodic_torsion = hoomd.md.dihedral.harmonic() for name, dihedral_type in dihedral_type_params.items(): periodic_torsion.dihedral_coeff.set( name, k=2 * dihedral_type.phi_k / ref_energy, d=1, n=dihedral_type.per, phi_0=np.deg2rad(dihedral_type.phase), ) return periodic_torsion
def from_uvec_lengths(cls, uvec, lengths, precision=None): """Generate a box from unit vectors and lengths.""" uvec = np.asarray(uvec) uvec.reshape(3, 3) if not np.allclose(np.linalg.norm(uvec, axis=1), 1.0): raise MBuildError( "Unit vector magnitudes provided are not close to 1.0, " f"magnitudes: {np.linalg.norm(uvec, axis=1)}") lengths = np.asarray(lengths) lengths.reshape(1, 3) _validate_box_vectors(uvec) scaled_vec = (uvec.T * lengths).T (alpha, beta, gamma) = _calc_angles(scaled_vec) return cls(lengths=lengths, angles=(alpha, beta, gamma), precision=precision)
def read_xyz(filename, compound=None): """Read an XYZ file. The expected format is as follows: The first line contains the number of atoms in the file. The second line contains a comment, which is not read. Remaining lines, one for each atom in the file, include an elemental symbol followed by X, Y, and Z coordinates in Angstroms. Columns are expected tbe separated by whitespace. See https://openbabel.org/wiki/XYZ_(format). Parameters ---------- filename : str Path of the input file Returns ------- compound : Compound Notes ----- The XYZ file format neglects many important details, including bonds, residues, and box information. There are some other flavors of the XYZ file format and not all are guaranteed to be compatible with this reader. For example, the TINKER XYZ format is not expected to be properly read. """ if compound is None: compound = mb.Compound() guessed_elements = set() with open(filename, "r") as xyz_file: n_atoms = int(xyz_file.readline()) xyz_file.readline() coords = np.zeros(shape=(n_atoms, 3), dtype=np.float64) for row, _ in enumerate(coords): line = xyz_file.readline().split() if not line: msg = ( "Incorrect number of lines in input file. Based on the " "number in the first line of the file, {} rows of atoms " "were expected, but at least one fewer was found.") raise MBuildError(msg.format(n_atoms)) coords[row] = line[1:4] coords[row] *= 0.1 name = line[0] try: element = name.capitalize() element = element_from_symbol(element) except ElementError: if name not in guessed_elements: warn("No matching element found for {}; the particle will " "be added to the compound without an element " "attribute.".format(name)) guessed_elements.add(name) element = None particle = mb.Compound(pos=coords[row], name=name, element=element) compound.add(particle) # Verify we have read the last line by ensuring the next line in blank line = xyz_file.readline().split() if line: msg = ("Incorrect number of lines in input file. Based on the " "number in the first line of the file, {} rows of atoms " "were expected, but at least one more was found.") raise MBuildError(msg.format(n_atoms)) return compound
def pos(self, value): if not self.children: self._pos = value else: raise MBuildError('Cannot set position on a Compound that has' ' children.')
def add(self, new_child, label=None, containment=True, replace=False, inherit_periodicity=True): """Add a part to the Compound. Note: This does not necessarily add the part to self.children but may instead be used to add a reference to the part to self.labels. See 'containment' argument. Parameters ---------- new_child : mb.Compound or list-like of mb.Compound The object(s) to be added to this Compound. label : str, optional A descriptive string for the part. containment : bool, optional, default=True Add the part to self.children. replace : bool, optional, default=True Replace the label if it already exists. """ # Support batch add via lists, tuples and sets. if (isinstance(new_child, collections.Iterable) and not isinstance(new_child, string_types)): for child in new_child: self.add(child) return if not isinstance(new_child, Compound): raise ValueError('Only objects that inherit from mbuild.Compound ' 'can be added to Compounds. You tried to add ' '"{}".'.format(new_child)) # Create children and labels on the first add operation if self.children is None: self.children = OrderedSet() if self.labels is None: self.labels = OrderedDict() if containment: if new_child.parent is not None: raise MBuildError('Part {} already has a parent: {}'.format( new_child, new_child.parent)) self.children.add(new_child) new_child.parent = self if new_child.bond_graph is not None: if self.root.bond_graph is None: self.root.bond_graph = new_child.bond_graph else: self.root.bond_graph.compose(new_child.bond_graph) new_child.bond_graph = None # Add new_part to labels. Does not currently support batch add. if label is None: label = '{0}[$]'.format(new_child.__class__.__name__) if label.endswith('[$]'): label = label[:-3] if label not in self.labels: self.labels[label] = [] label_pattern = label + '[{}]' count = len(self.labels[label]) self.labels[label].append(new_child) label = label_pattern.format(count) if not replace and label in self.labels: raise MBuildError('Label "{0}" already exists in {1}.'.format( label, self)) else: self.labels[label] = new_child new_child.referrers.add(self) if (inherit_periodicity and isinstance(new_child, Compound) and new_child.periodicity.any()): self.periodicity = new_child.periodicity