diff --git a/mbuild/compound.py b/mbuild/compound.py index 96f6d95d6..21453a114 100644 --- a/mbuild/compound.py +++ b/mbuild/compound.py @@ -2979,15 +2979,9 @@ def save( self, filename, include_ports=False, - forcefield_name=None, - forcefield_files=None, - forcefield_debug=False, box=None, overwrite=False, residues=None, - combining_rule="lorentz", - foyer_kwargs=None, - parmed_kwargs=None, **kwargs, ): """Save the Compound to a file. @@ -3002,17 +2996,6 @@ def save( information on savers. include_ports : bool, optional, default=False Save ports contained within the compound. - forcefield_files : str, optional, default=None - Apply a forcefield to the output file using a forcefield provided - by the `foyer` package. - forcefield_name : str, optional, default=None - Apply a named forcefield to the output file using the `foyer` - package, e.g. 'oplsaa'. `Foyer forcefields - `_ - forcefield_debug : bool, optional, default=False - Choose verbosity level when applying a forcefield through `foyer`. - Specifically, when missing atom types in the forcefield xml file, - determine if the warning is condensed or verbose. box : mb.Box, optional, default=self.boundingbox (with buffer) Box information to be written to the output file. If 'None', a bounding box is used with 0.25nm buffers at each face to avoid @@ -3022,20 +3005,7 @@ def save( residues : str of list of str Labels of residues in the Compound. Residues are assigned by checking against Compound.name. - combining_rule : str, optional, default='lorentz' - Specify the combining rule for nonbonded interactions. Only relevant - when the `foyer` package is used to apply a forcefield. Valid - options are 'lorentz' and 'geometric', specifying Lorentz-Berthelot - and geometric combining rules respectively. - foyer_kwargs : dict, optional, default=None - Keyword arguments to provide to `foyer.Forcefield.apply`. - Depending on the file extension these will be passed to either - `write_gsd`, `write_lammpsdata`, - `write_mcf`, or `parmed.Structure.save`. - See `parmed structure documentation - `_ - parmed_kwargs : dict, optional, default=None - Keyword arguments to provide to :meth:`mbuild.Compound.to_parmed` + #TODO 1.0: Update this kwargs, pass link to GMSO **kwargs Depending on the file extension these will be passed to either `write_gsd`, `write_lammpsdata`, `write_mcf`, or @@ -3084,18 +3054,12 @@ def save( formats.json_formats.compound_to_json : Write to a json file """ conversion.save( - self, - filename, - include_ports, - forcefield_name, - forcefield_files, - forcefield_debug, - box, - overwrite, - residues, - combining_rule, - foyer_kwargs, - parmed_kwargs, + compound=self, + filename=filename, + include_ports=include_ports, + box=box, + overwrite=overwrite, + residues=residues, **kwargs, ) diff --git a/mbuild/conversion.py b/mbuild/conversion.py index c1bc968c4..a57f9ded3 100644 --- a/mbuild/conversion.py +++ b/mbuild/conversion.py @@ -20,7 +20,6 @@ import mbuild as mb from mbuild.box import Box from mbuild.exceptions import MBuildError -from mbuild.formats.hoomd_writer import write_gsd from mbuild.formats.json_formats import compound_from_json, compound_to_json from mbuild.formats.par_writer import write_par from mbuild.formats.xyz import read_xyz, write_xyz @@ -1015,6 +1014,9 @@ def save( formats.cassandramcf.write_mcf : Write to Cassandra MCF format formats.json_formats.compound_to_json : Write to a json file """ + if os.path.exists(filename) and not overwrite: + raise IOError(f"{filename} exists; not overwriting") + extension = os.path.splitext(filename)[-1] # Keep json stuff with internal mbuild method if extension == ".json": @@ -1026,6 +1028,9 @@ def save( # Savers supported by mbuild.formats # TODO 1.0: Will the CHARMM par writer work with non-typed systems? Do we support writing it from mbuild? # TODO 1.0: Do we update the par writer to skip angles, dihedrals, Parameters, etc.. and just write xyz and bonds? + # TODO 1.0: Do we have a pdb writer anywhere? Right now, we use parmed + # TODO 1.0: GMSO can't save mol2 files, do we prioritize a mol2 writer, or continue using parmed backend here? + # TODO 1.0: Is there ever a need to save .lammps, .lammpsdata files that don't have a FF applied? savers = { ".gro": save_in_gmso, ".gsd": save_in_gmso, @@ -1033,7 +1038,7 @@ def save( ".lammpsdata": save_in_gmso, ".data": save_in_gmso, ".xyz": save_in_gmso, - ".mol2": save_in_gmso, + # ".mol2": save_in_gmso, ".mcf": save_in_gmso, ".top": save_in_gmso, ".par": write_par, @@ -1043,10 +1048,6 @@ def save( saver = savers[extension] except KeyError: saver = None - - if os.path.exists(filename) and not overwrite: - raise IOError(f"{filename} exists; not overwriting") - # Provide a warning if rigid_ids are not sequential from 0 if compound.contains_rigid: unique_rigid_ids = sorted( @@ -1059,29 +1060,33 @@ def save( if extension == ".gsd": kwargs["rigid_bodies"] = [p.rigid_id for p in compound.particles()] # Calling save_in_gmso - saver(filename=filename, compound=structure, box=box, **kwargs) + saver( + filename=filename, + compound=compound, + box=box, + overwrite=overwrite, + **kwargs, + ) elif extension == ".sdf": pybel = import_("pybel") - new_compound = mb.Compound() - # Convert pmd.Structure to mb.Compound - new_compound.from_parmed(structure) - # Convert mb.Compound to pybel molecule - pybel_molecule = new_compound.to_pybel() + pybel_molecule = compound.to_pybel() # Write out pybel molecule to SDF file output_sdf = pybel.Outputfile("sdf", filename, overwrite=overwrite) output_sdf.write(pybel_molecule) output_sdf.close() # TODO 1.0: Keep this to catch any file types not supported by GMSO? else: # ParmEd supported saver. + structure = compound.to_parmed() structure.save(filename, overwrite=overwrite, **kwargs) # TODO 1.0: Add doc strings, links, etc.. -def save_in_gmso(compound, filename, box, **kwargs): +def save_in_gmso(compound, filename, box, overwrite, **kwargs): """Convert to GMSO, call gmso writers.""" - gmso_top = to_gmso(compound=compound, box=box, **kwargs) - gmso_top.save(filename, **kwargs) + # TODO: Pass in rigid body tags here when added to GMSO + gmso_top = to_gmso(compound=compound, box=box) + gmso_top.save(filename=filename, overwrite=overwrite, **kwargs) def catalog_bondgraph_type(compound, bond_graph=None): @@ -1904,7 +1909,14 @@ def _iterate_children(compound, nodes, edges, names_only=False): return nodes, edges -def to_gmso(compound, box=None, **kwargs): +def to_gmso( + compound, + box=None, + parse_label=True, + custom_groups=None, + infer_elements=False, + **kwargs, +): """Create a GMSO Topology from a mBuild Compound. Parameters @@ -1921,7 +1933,14 @@ def to_gmso(compound, box=None, **kwargs): """ from gmso.external.convert_mbuild import from_mbuild - return from_mbuild(compound, box=None, **kwargs) + # TODO: Pass in rigid body IDs here once added to GMSO + return from_mbuild( + compound=compound, + box=box, + parse_label=parse_label, + custom_groups=custom_groups, + infer_elements=infer_elements, + ) def to_intermol(compound, molecule_types=None): # pragma: no cover diff --git a/mbuild/tests/test_compound.py b/mbuild/tests/test_compound.py index 84fd99d60..8ba662e63 100644 --- a/mbuild/tests/test_compound.py +++ b/mbuild/tests/test_compound.py @@ -373,12 +373,25 @@ def test_load_protein(self): # Chains info is lossed assert len(protein.children) == 393 - def test_save_simple(self, ch3): - extensions = [".xyz", ".pdb", ".mol2", ".json", ".sdf"] - for ext in extensions: - outfile = "methyl_out" + ext - ch3.save(filename=outfile) - assert os.path.exists(outfile) + @pytest.mark.parametrize( + "extension", + [ + ".xyz", + ".pdb", + ".mol2", + # ".gro", + ".gsd", + ".json", + # ".top", + ".mcf", + # ".lammps", + ".sdf", + ], + ) + def test_save_simple(self, ch3, extension): + outfile = "methyl_out" + extension + ch3.save(filename=outfile) + assert os.path.exists(outfile) def test_save_json_loop(self, ethane): ethane.save("ethane.json", show_ports=True) @@ -412,63 +425,6 @@ def test_save_overwrite(self, ch3): with pytest.raises(IOError): ch3.save(filename=outfile, overwrite=False) - @pytest.mark.skipif(not has_foyer, reason="Foyer is not installed") - def test_save_forcefield(self, methane): - exts = [ - ".gsd", - ".top", - ".gro", - ".mol2", - ".pdb", - ".xyz", - ".sdf", - ] - for ext in exts: - methane.save( - "lythem" + ext, forcefield_name="oplsaa", overwrite=True - ) - - @pytest.mark.skipif(not has_foyer, reason="Foyer is not installed") - def test_save_forcefield_with_file(self, methane): - exts = [ - ".gsd", - ".top", - ".gro", - ".mol2", - ".pdb", - ".xyz", - ".sdf", - ] - for ext in exts: - methane.save( - "lythem" + ext, - forcefield_files=get_fn("methane_oplssaa.xml"), - overwrite=True, - ) - - @pytest.mark.skipif(not has_foyer, reason="Foyer is not installed") - @pytest.mark.parametrize( - "ff_filename,foyer_kwargs", - [ - ("ethane-angle-typo.xml", {"assert_angle_params": False}), - ("ethane-dihedral-typo.xml", {"assert_dihedral_params": False}), - ], - ) - def test_save_missing_topo_params(self, ff_filename, foyer_kwargs): - """Test that the user is notified if not all topology parameters are found.""" - from foyer.tests.utils import get_fn - - ethane = mb.load(get_fn("ethane.mol2")) - with pytest.raises(Exception): - ethane.save("ethane.mol2", forcefield_files=get_fn(ff_filename)) - with pytest.warns(UserWarning): - ethane.save( - "ethane.mol2", - forcefield_files=get_fn(ff_filename), - overwrite=True, - foyer_kwargs=foyer_kwargs, - ) - def test_save_resnames(self, ch3, h2o): system = Compound([ch3, h2o]) system.save("resnames.gro", residues=["CH3", "H2O"]) @@ -484,63 +440,6 @@ def test_save_resnames_single(self, c3, n4): assert struct.residues[0].number == 1 assert struct.residues[1].number == 2 - def test_save_residue_map(self, ethane): - filled = mb.fill_box(ethane, n_compounds=100, box=[0, 0, 0, 4, 4, 4]) - t0 = time.time() - foyer_kwargs = {"use_residue_map": True} - filled.save( - "filled.mol2", - forcefield_name="oplsaa", - residues="Ethane", - foyer_kwargs=foyer_kwargs, - ) - t1 = time.time() - - foyer_kwargs = {"use_residue_map": False} - filled.save( - "filled.mol2", - forcefield_name="oplsaa", - overwrite=True, - residues="Ethane", - foyer_kwargs=foyer_kwargs, - ) - t2 = time.time() - assert (t2 - t1) > (t1 - t0) - - @pytest.mark.skipif(not has_foyer, reason="Foyer is not installed") - def test_save_references(self, methane): - foyer_kwargs = {"references_file": "methane.bib"} - methane.save( - "methyl.mol2", forcefield_name="oplsaa", foyer_kwargs=foyer_kwargs - ) - assert os.path.isfile("methane.bib") - - @pytest.mark.skipif(not has_foyer, reason="Foyer is not installed") - def test_save_combining_rule(self, methane): - combining_rules = ["lorentz", "geometric"] - gmx_rules = {"lorentz": 2, "geometric": 3} - for combining_rule in combining_rules: - if combining_rule == "geometric": - methane.save( - "methane.top", - forcefield_name="oplsaa", - combining_rule=combining_rule, - overwrite=True, - ) - else: - with pytest.warns(UserWarning): - methane.save( - "methane.top", - forcefield_name="oplsaa", - combining_rule=combining_rule, - overwrite=True, - ) - with open("methane.top") as fp: - for i, line in enumerate(fp): - if i == 18: - gmx_rule = int(line.split()[1]) - assert gmx_rule == gmx_rules[combining_rule] - def test_clone_with_box(self, ethane): ethane.box = ethane.get_boundingbox() ethane.periodicity = (True, True, False)