def test_convert_kwarg_units_helper(self): kwargs = { "example_cutoff": 5.5 * u.angstrom, "example_cutoff_list": [5.5 * u.angstrom, 6.0 * u.angstrom], "example_cutoff_array": [5.5, 6.0] * u.angstrom, } _convert_kwarg_units_helper(kwargs, "example_cutoff", "nm") assert kwargs["example_cutoff"].units == u.nm assert allclose_units(kwargs["example_cutoff"], 0.55 * u.nm) with pytest.raises(TypeError, match="went wrong"): _convert_kwarg_units_helper(kwargs, "example_cutoff_list", "nm") _convert_kwarg_units_helper(kwargs, "example_cutoff_array", "nm") assert kwargs["example_cutoff_array"].units == u.nm assert allclose_units(kwargs["example_cutoff_array"], [0.55, 0.60] * u.nm)
def __eq__(self, other): """Compare two boxes for equivalence.""" if self is other: return True if not isinstance(other, Box): return False if not allclose_units( self.lengths, other.lengths, rtol=1e-5, atol=1e-8): return False if not allclose_units(self.angles, other.angles, rtol=1e-5, atol=1e-8): return False return True
def assert_allclose_units(actual, desired, rtol=1e-7, atol=0, **kwargs): """Raise an error if two objects are not equal up to desired tolerance This is a wrapper for :func:`numpy.testing.assert_allclose` that also verifies unit consistency Parameters ---------- actual : array-like Array obtained (possibly with attached units) desired : array-like Array to compare with (possibly with attached units) rtol : float, optional Relative tolerance, defaults to 1e-7 atol : float or quantity, optional Absolute tolerance. If units are attached, they must be consistent with the units of ``actual`` and ``desired``. If no units are attached, assumes the same units as ``desired``. Defaults to zero. See Also -------- :func:`unyt.array.allclose_units` Notes ----- Also accepts additional keyword arguments accepted by :func:`numpy.testing.assert_allclose`, see the documentation of that function for details. Examples -------- >>> import unyt as u >>> actual = [1e-5, 1e-3, 1e-1]*u.m >>> desired = actual.to("cm") >>> assert_allclose_units(actual, desired) """ if not allclose_units(actual, desired, rtol, atol, **kwargs): raise AssertionError
def write_gro(top, filename): """Write a topology to a gro file. The Gromos87 (gro) format is a common plain text structure file used commonly with the GROMACS simulation engine. This file contains the simulation box parameters, number of atoms, the residue and atom number for each atom, as well as their positions and velocities (velocity is optional). This method takes a topology object and a filepath string or file object and saves a Gromos87 (gro) to disk. Parameters ---------- top : gmso.core.topology The `topology` to write out to the gro file. filename : str or file object The location and name of file to save to disk. Notes ----- Multiple residue assignment has not been added, each `site` will belong to the same resid of 1 currently. """ top = _prepare_topology_to_gro(top) with open(filename, 'w') as out_file: out_file.write('{} written by topology at {}\n'.format( top.name if top.name is not None else '', str(datetime.datetime.now()))) out_file.write('{:d}\n'.format(top.n_sites)) for idx, site in enumerate(top.sites): warnings.warn( 'Residue information is not currently ' 'stored or written to GRO files.', NotYetImplementedWarning) # TODO: assign residues res_id = 1 res_name = 'X' atom_name = site.name atom_id = idx + 1 out_file.write( '{0:5d}{1:5s}{2:5s}{3:5d}{4:8.3f}{5:8.3f}{6:8.3f}\n'.format( res_id, res_name, atom_name, atom_id, site.position[0].in_units(u.nm).value, site.position[1].in_units(u.nm).value, site.position[2].in_units(u.nm).value, )) if allclose_units(top.box.angles, u.degree * [90, 90, 90], rtol=1e-5, atol=0.1 * u.degree): out_file.write(' {:0.5f} {:0.5f} {:0.5f} \n'.format( top.box.lengths[0].in_units(u.nm).value.round(6), top.box.lengths[1].in_units(u.nm).value.round(6), top.box.lengths[2].in_units(u.nm).value.round(6), )) else: # TODO: Work around GROMACS's triclinic limitations #30 vectors = top.box.get_vectors() out_file.write( ' {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} {:0.5f} \n' .format( vectors[0, 0].in_units(u.nm).value.round(6), vectors[1, 1].in_units(u.nm).value.round(6), vectors[2, 2].in_units(u.nm).value.round(6), vectors[0, 1].in_units(u.nm).value.round(6), vectors[0, 2].in_units(u.nm).value.round(6), vectors[1, 0].in_units(u.nm).value.round(6), vectors[1, 2].in_units(u.nm).value.round(6), vectors[2, 0].in_units(u.nm).value.round(6), vectors[2, 1].in_units(u.nm).value.round(6), ))
def write_lammpsdata(topology, filename, atom_style="full"): """Output a LAMMPS data file. Outputs a LAMMPS data file in the 'full' atom style format. Assumes use of 'real' units. See http://lammps.sandia.gov/doc/atom_style.html for more information on atom styles. Parameters ---------- Topology : `Topology` A Topology Object filename : str Path of the output file atom_style : str, optional, default='full' Defines the style of atoms to be saved in a LAMMPS data file. The following atom styles are currently supported: 'full', 'atomic', 'charge', 'molecular' see http://lammps.sandia.gov/doc/atom_style.html for more information on atom styles. Notes ----- See http://lammps.sandia.gov/doc/2001/data_format.html for a full description of the LAMMPS data format. This is a work in progress, as only atoms, masses, and atom_type information can be written out. Some of this function has been adopted from `mdtraj`'s support of the LAMMPSTRJ trajectory format. See https://github.com/mdtraj/mdtraj/blob/master/mdtraj/formats/lammpstrj.py for details. """ if atom_style not in ["atomic", "charge", "molecular", "full"]: raise ValueError( 'Atom style "{}" is invalid or is not currently supported'.format( atom_style)) # TODO: Support various unit styles box = topology.box with open(filename, "w") as data: data.write("{} written by topology at {}\n\n".format( topology.name if topology.name is not None else "", str(datetime.datetime.now()), )) data.write("{:d} atoms\n".format(topology.n_sites)) if atom_style in ["full", "molecular"]: if topology.n_bonds != 0: data.write("{:d} bonds\n".format(topology.n_bonds)) else: data.write("0 bonds\n") if topology.n_angles != 0: data.write("{:d} angles\n".format(topology.n_angles)) else: data.write("0 angles\n") if topology.n_dihedrals != 0: data.write("{:d} dihedrals\n\n".format(topology.n_dihedrals)) else: data.write("0 dihedrals\n\n") data.write("\n{:d} atom types\n".format(len(topology.atom_types))) data.write("{:d} bond types\n".format(len(topology.bond_types))) data.write("{:d} angle types\n".format(len(topology.angle_types))) data.write("{:d} dihedral types\n".format(len( topology.dihedral_types))) data.write("\n") # Box data if allclose_units( box.angles, u.unyt_array([90, 90, 90], "degree"), rtol=1e-5, atol=1e-8, ): warnings.warn("Orthorhombic box detected") box.lengths.convert_to_units(u.angstrom) for i, dim in enumerate(["x", "y", "z"]): data.write("{0:.6f} {1:.6f} {2}lo {2}hi\n".format( 0, box.lengths.value[i], dim)) else: warnings.warn("Non-orthorhombic box detected") box.lengths.convert_to_units(u.angstrom) box.angles.convert_to_units(u.radian) vectors = box.get_vectors() a, b, c = box.lengths alpha, beta, gamma = box.angles lx = a xy = b * np.cos(gamma) xz = c * np.cos(beta) ly = np.sqrt(b**2 - xy**2) yz = (b * c * np.cos(alpha) - xy * xz) / ly lz = np.sqrt(c**2 - xz**2 - yz**2) xhi = vectors[0][0] yhi = vectors[1][1] zhi = vectors[2][2] xy = vectors[1][0] xz = vectors[2][0] yz = vectors[2][1] xlo = u.unyt_array(0, xy.units) ylo = u.unyt_array(0, xy.units) zlo = u.unyt_array(0, xy.units) xlo_bound = xlo + u.unyt_array(np.min([0.0, xy, xz, xy + xz]), xy.units) xhi_bound = xhi + u.unyt_array(np.max([0.0, xy, xz, xy + xz]), xy.units) ylo_bound = ylo + u.unyt_array(np.min([0.0, yz]), xy.units) yhi_bound = yhi + u.unyt_array(np.max([0.0, yz]), xy.units) zlo_bound = zlo zhi_bound = zhi data.write("{0:.6f} {1:.6f} xlo xhi\n".format( xlo_bound.value, xhi_bound.value)) data.write("{0:.6f} {1:.6f} ylo yhi\n".format( ylo_bound.value, yhi_bound.value)) data.write("{0:.6f} {1:.6f} zlo zhi\n".format( zlo_bound.value, zhi_bound.value)) data.write("{0:.6f} {1:.6f} {2:.6f} xy xz yz\n".format( xy.value, xz.value, yz.value)) # TODO: Get a dictionary of indices and atom types if topology.is_typed(): # Write out mass data data.write("\nMasses\n\n") for atom_type in topology.atom_types: data.write("{:d}\t{:.6f}\t# {}\n".format( topology.atom_types.index(atom_type) + 1, atom_type.mass.in_units(u.g / u.mol).value, atom_type.name, )) # TODO: Modified cross-interactions # Pair coefficients data.write("\nPair Coeffs # lj\n\n") for idx, param in enumerate(topology.atom_types): data.write("{}\t{:.5f}\t{:.5f}\n".format( idx + 1, param.parameters["epsilon"].in_units( u.Unit("kcal/mol")).value, param.parameters["sigma"].in_units(u.angstrom).value, )) if topology.bonds: data.write("\nBond Coeffs\n\n") for idx, bond_type in enumerate(topology.bond_types): data.write("{}\t{:.5f}\t{:.5f}\n".format( idx + 1, bond_type.parameters["k"].in_units( u.Unit("kcal/mol/angstrom**2")).value / 2, bond_type.parameters["r_eq"].in_units( u.Unit("angstrom")).value, )) if topology.angles: data.write("\nAngle Coeffs\n\n") for idx, angle_type in enumerate(topology.angle_types): data.write("{}\t{:.5f}\t{:.5f}\n".format( idx + 1, angle_type.parameters["k"].in_units( u.Unit("kcal/mol/radian**2")).value / 2, angle_type.parameters["theta_eq"].in_units( u.Unit("degree")).value, )) # TODO: Write out multiple dihedral styles if topology.dihedrals: data.write("\nDihedral Coeffs\n\n") for idx, dihedral_type in enumerate(topology.dihedral_types): rbtorsion = PotentialTemplateLibrary( )["RyckaertBellemansTorsionPotential"] if (dihedral_type.expression == sympify( rbtorsion.expression) or dihedral_type.name == rbtorsion.name): dihedral_type = convert_ryckaert_to_opls(dihedral_type) data.write("{}\t{:.5f}\t{:5f}\t{:5f}\t{:.5f}\n".format( idx + 1, dihedral_type.parameters["k1"].in_units( u.Unit("kcal/mol")).value, dihedral_type.parameters["k2"].in_units( u.Unit("kcal/mol")).value, dihedral_type.parameters["k3"].in_units( u.Unit("kcal/mol")).value, dihedral_type.parameters["k4"].in_units( u.Unit("kcal/mol")).value, )) # Atom data data.write("\nAtoms\n\n") if atom_style == "atomic": atom_line = "{index:d}\t{type_index:d}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n" elif atom_style == "charge": atom_line = "{index:d}\t{type_index:d}\t{charge:.6f}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n" elif atom_style == "molecular": atom_line = "{index:d}\t{zero:d}\t{type_index:d}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n" elif atom_style == "full": atom_line = "{index:d}\t{zero:d}\t{type_index:d}\t{charge:.6f}\t{x:.6f}\t{y:.6f}\t{z:.6f}\n" for i, site in enumerate(topology.sites): data.write( atom_line.format( index=topology.sites.index(site) + 1, type_index=topology.atom_types.index(site.atom_type) + 1, zero=0, charge=site.charge.to(u.elementary_charge).value, x=site.position[0].in_units(u.angstrom).value, y=site.position[1].in_units(u.angstrom).value, z=site.position[2].in_units(u.angstrom).value, )) if topology.bonds: data.write("\nBonds\n\n") for i, bond in enumerate(topology.bonds): data.write("{:d}\t{:d}\t{:d}\t{:d}\n".format( i + 1, topology.bond_types.index(bond.connection_type) + 1, topology.sites.index(bond.connection_members[0]) + 1, topology.sites.index(bond.connection_members[1]) + 1, )) if topology.angles: data.write("\nAngles\n\n") for i, angle in enumerate(topology.angles): data.write("{:d}\t{:d}\t{:d}\t{:d}\t{:d}\n".format( i + 1, topology.angle_types.index(angle.connection_type) + 1, topology.sites.index(angle.connection_members[0]) + 1, topology.sites.index(angle.connection_members[1]) + 1, topology.sites.index(angle.connection_members[2]) + 1, )) if topology.dihedrals: data.write("\nDihedrals\n\n") for i, dihedral in enumerate(topology.dihedrals): data.write("{:d}\t{:d}\t{:d}\t{:d}\t{:d}\t{:d}\n".format( i + 1, topology.dihedral_types.index(dihedral.connection_type) + 1, topology.sites.index(dihedral.connection_members[0]) + 1, topology.sites.index(dihedral.connection_members[1]) + 1, topology.sites.index(dihedral.connection_members[2]) + 1, topology.sites.index(dihedral.connection_members[3]) + 1, ))
def write_gsd( top, filename, ref_distance=1.0 * u.nm, ref_mass=1.0 * u.Unit("g/mol"), ref_energy=1.0 * u.Unit("kcal/mol"), rigid_bodies=None, shift_coords=True, write_special_pairs=True, ): """Output a GSD file (HOOMD v2 default data format). The `GSD` binary file format is the native format of HOOMD-Blue. This file can be used as a starting point for a HOOMD-Blue simulation, for analysis, and for visualization in various tools. Parameters ---------- top : gmso.Topology gmso.Topology object filename : str Path of the output file. ref_distance : float, optional, default=1.0 Reference distance for conversion to reduced units ref_mass : float, optional, default=1.0 Reference mass for conversion to reduced units ref_energy : float, optional, default=1.0 Reference energy for conversion to reduced units rigid_bodies : list of int, optional, default=None List of rigid body information. An integer value is required for each atom corresponding to the index of the rigid body the particle is to be associated with. A value of None indicates the atom is not part of a rigid body. shift_coords : bool, optional, default=True Shift coordinates from (0, L) to (-L/2, L/2) if necessary. write_special_pairs : bool, optional, default=True Writes out special pair information necessary to correctly use the OPLS fudged 1,4 interactions in HOOMD. Notes ----- Force field parameters are not written to the GSD file and must be included manually in a HOOMD input script. Work on a HOOMD plugin is underway to read force field parameters from a Foyer XML file. """ xyz = u.unyt_array([site.position for site in top.sites]) if shift_coords: warnings.warn("Shifting coordinates to [-L/2, L/2]") xyz = coord_shift(xyz, top.box) gsd_snapshot = gsd.hoomd.Snapshot() gsd_snapshot.configuration.step = 0 gsd_snapshot.configuration.dimensions = 3 # Write box information if allclose_units(top.box.angles, np.array([90, 90, 90]) * u.degree, rtol=1e-5, atol=1e-8): warnings.warn("Orthorhombic box detected") gsd_snapshot.configuration.box = np.hstack( (top.box.lengths / ref_distance, np.zeros(3))) else: warnings.warn("Non-orthorhombic box detected") u_vectors = top.box.get_unit_vectors() lx, ly, lz = top.box.lengths / ref_distance xy = u_vectors[1][0] xz = u_vectors[2][0] yz = u_vectors[2][1] gsd_snapshot.configuration.box = np.array([lx, ly, lz, xy, xz, yz]) warnings.warn( "Only writing particle and bond information." " Angle and dihedral is not currently written to GSD files", NotYetImplementedWarning, ) _write_particle_information(gsd_snapshot, top, xyz, ref_distance, ref_mass, ref_energy, rigid_bodies) # if write_special_pairs: # _write_pair_information(gsd_snapshot, top) if top.n_bonds > 0: _write_bond_information(gsd_snapshot, top) # if structure.angles: # _write_angle_information(gsd_snapshot, top) # if structure.rb_torsions: # _write_dihedral_information(gsd_snapshot, top) with gsd.hoomd.open(filename, mode="wb") as gsd_file: gsd_file.append(gsd_snapshot)