From 966bea580dc0886feb697f1b2bc619160e4d8b74 Mon Sep 17 00:00:00 2001 From: "Josef M. Gallmetzer" <64498081+galjos@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:08:08 +0200 Subject: [PATCH] Add GBSA/ALPB implicit solvation to the DFTB+ pipeline Add a `solvent=` knob so geometry optimisation, energy, and the Hessian all run in implicit solvent. DFTB+ includes the GBSA/SASA term in the second derivatives, so the optimised geometry and the frequencies stay consistent with the solvated energy. - calculator/dftbplus.py: `_solvation_kwargs` builds the `Solvation = GeneralizedBorn { ParamFile = ... }` block (empty for the gas-phase default), passing an absolute parameter-file path since DFTB+ resolves it relative to the run directory. - cli/dftb_setup.py: download GBSA parameter files (grimme-lab/gbsa-parameters) by solvent name; `setup-dftb --solvent ` fetches them. - thermo/api.py, thermo/screening.py, cli/thermo.py: thread `solvent` (and an explicit `solvation_param_file` override) through `dftbplus_thermo`, `screen`, and the `screen`/`setup-dftb` CLI. - thermo/screening.py: report `Eelec_hartree` (the electronic energy, which carries the solvation term) and `G_total_hartree` (electronic + Gibbs correction), so the solvation free energy is visible in the results. The published GBSA parameters are fit for GFN-xTB; used with 3ob/mio they are an approximation, so an explicit method-consistent parameter file can be supplied instead. Validated against real DFTB+: solvated water is stabilised by ~14 kcal/mol and the effect appears in the reported free energy. --- ThermoScreening/calculator/dftbplus.py | 56 +++++++++++++ ThermoScreening/cli/dftb_setup.py | 112 +++++++++++++++++++++++++ ThermoScreening/cli/thermo.py | 25 +++++- ThermoScreening/thermo/api.py | 21 ++++- ThermoScreening/thermo/screening.py | 13 +++ tests/calculator/test_dftbplus.py | 27 ++++++ tests/cli/test_dftb_setup.py | 57 +++++++++++++ tests/thermo/test_api.py | 27 ++++++ tests/thermo/test_main.py | 24 ++++++ tests/thermo/test_screening.py | 34 ++++++++ 10 files changed, 392 insertions(+), 4 deletions(-) diff --git a/ThermoScreening/calculator/dftbplus.py b/ThermoScreening/calculator/dftbplus.py index bac71ee..f28a896 100644 --- a/ThermoScreening/calculator/dftbplus.py +++ b/ThermoScreening/calculator/dftbplus.py @@ -141,6 +141,62 @@ def _spin_kwargs(atoms, spin, spin_constants=SPIN_CONSTANTS_3OB): return kwargs +def _solvation_kwargs(solvent=None, param_file=None, install_root=None): + """ + ASE ``Dftb`` keyword arguments enabling GBSA/ALPB implicit solvation. + + Returns an empty dict when neither ``solvent`` nor ``param_file`` is given, + so the gas-phase calculation is left exactly as before. Otherwise it enables + ``Solvation = GeneralizedBorn`` with the solvent's parameter file. DFTB+ + includes the solvation term in the energy, gradient, and Hessian, so the + geometry, energy, and frequencies are all computed in solution consistently. + + The parameter files (grimme-lab/gbsa-parameters) are fit for GFN-xTB; used + with the DFTB (3ob/mio) Hamiltonians they are an approximation. Pass an + explicit ``param_file`` to use a method-consistent set instead. + + Parameters + ---------- + solvent : str, optional + Solvent name (e.g. ``"water"``). Resolved to a downloaded parameter file. + param_file : str, optional + Explicit path to a GBSA parameter file (overrides ``solvent``). + install_root : str or Path, optional + Root used to locate downloaded solvent parameters. + + Raises + ------ + FileNotFoundError + If the resolved parameter file does not exist. + """ + if solvent is None and param_file is None: + return {} + + if param_file is not None: + path = os.path.abspath(os.path.expanduser(str(param_file))) + if not os.path.isfile(path): + raise FileNotFoundError( + f"GBSA solvation parameter file does not exist: {path}" + ) + else: + # Lazy import: this is an environment/paths lookup, not a core dependency. + from ..cli.dftb_setup import gbsa_param_path + + path = os.path.abspath(str(gbsa_param_path(solvent, install_root))) + if not os.path.isfile(path): + raise FileNotFoundError( + f"No GBSA parameter file for solvent {solvent!r} at {path}. " + f"Download it first, e.g. `thermo setup-dftb --solvent {solvent}`." + ) + + # DFTB+ resolves ParamFile relative to the run directory, so pass an absolute + # path (jobs run in per-molecule working directories). + return { + "Hamiltonian_Solvation": "GeneralizedBorn {", + "Hamiltonian_Solvation_ParamFile": path, + } + + class Geoopt(Dftb): """ Custom DFTB+ calculator to optimize the system with the 'GeometryOptimisation' driver (Rational). diff --git a/ThermoScreening/cli/dftb_setup.py b/ThermoScreening/cli/dftb_setup.py index 2f20f01..a0c8a4b 100644 --- a/ThermoScreening/cli/dftb_setup.py +++ b/ThermoScreening/cli/dftb_setup.py @@ -167,6 +167,118 @@ def install_slakos( return parameter_dir.resolve() +# GBSA/ALPB implicit-solvation parameter files (grimme-lab/gbsa-parameters). The +# published sets are fit for GFN-xTB; used with DFTB (3ob/mio) they are an +# approximation (see the note in ``calculator.dftbplus._solvation_kwargs``). +GBSA_PARAM_METHOD = "gfn2-0-1" +GBSA_BASE_URL = "https://raw.githubusercontent.com/grimme-lab/gbsa-parameters/main" + +# User-facing solvent name -> parameter-file stem in the repository. +GBSA_SOLVENTS = { + "acetone": "acetone", + "acetonitrile": "acetonitrile", + "benzene": "benzene", + "ch2cl2": "ch2cl2", + "dichloromethane": "ch2cl2", + "chcl3": "chcl3", + "chloroform": "chcl3", + "cs2": "cs2", + "dmf": "dmf", + "dmso": "dmso", + "ether": "ether", + "diethylether": "ether", + "water": "h2o", + "h2o": "h2o", + "methanol": "methanol", + "hexane": "nhexane", + "nhexane": "nhexane", + "thf": "thf", + "toluene": "toluene", +} + + +def _solvent_stem(solvent: str) -> str: + """ + Map a user-facing solvent name to its parameter-file stem. + + Raises + ------ + ValueError + If ``solvent`` is not a known solvent. + """ + + try: + return GBSA_SOLVENTS[solvent.lower()] + except KeyError: + known = ", ".join(sorted(set(GBSA_SOLVENTS))) + raise ValueError( + f"Unknown solvent {solvent!r}; choose one of: {known}." + ) + + +def default_gbsa_dir(install_root: str | Path | None = None) -> Path: + """ + Return the directory holding downloaded GBSA solvation parameter files. + """ + + root = Path(install_root).expanduser() if install_root is not None else default_install_root() + return root / "gbsa" / GBSA_PARAM_METHOD + + +def gbsa_param_path( + solvent: str, + install_root: str | Path | None = None, +) -> Path: + """ + Return the local path where the GBSA parameter file for ``solvent`` lives + (whether or not it has been downloaded yet). + """ + + return default_gbsa_dir(install_root) / f"param_gbsa_{_solvent_stem(solvent)}.txt" + + +def install_gbsa_param( + solvent: str, + install_root: str | Path | None = None, + url: str | None = None, + force: bool = False, +) -> Path: + """ + Download the GBSA implicit-solvation parameter file for ``solvent``. + + Parameters + ---------- + solvent : str + Solvent name (e.g. ``"water"``); see :data:`GBSA_SOLVENTS`. + install_root : str or Path, optional + Directory root for downloaded parameters. Defaults to the user-local + share directory. + url : str, optional + Explicit file URL override. Defaults to the grimme-lab release file. + force : bool + Re-download even when the file already exists. + """ + + stem = _solvent_stem(solvent) + destination = gbsa_param_path(solvent, install_root) + + if destination.exists() and not force: + return destination.resolve() + + if url is None: + url = f"{GBSA_BASE_URL}/{GBSA_PARAM_METHOD}/param_gbsa_{stem}.txt" + + destination.parent.mkdir(parents=True, exist_ok=True) + _download_file(url, destination) + + if not destination.exists(): + raise FileNotFoundError( + f"Downloaded GBSA parameter file is missing: {destination}" + ) + + return destination.resolve() + + def dftb_prefix_export(parameter_dir: str | Path) -> str: """ Return the shell export line for a Slater-Koster parameter directory. diff --git a/ThermoScreening/cli/thermo.py b/ThermoScreening/cli/thermo.py index 89a5258..23b5240 100644 --- a/ThermoScreening/cli/thermo.py +++ b/ThermoScreening/cli/thermo.py @@ -8,6 +8,7 @@ check_dftb_setup, dftb_prefix_export, format_diagnostics, + install_gbsa_param, install_slakos, ) from ThermoScreening.thermo.api import execute @@ -51,6 +52,12 @@ def _command_parser(): default=None, help="Archive URL override (defaults to the release URL for the set).", ) + setup_parser.add_argument( + "--solvent", + default=None, + help="Instead of a parameter set, download the GBSA implicit-solvation " + "parameter file for this solvent (e.g. 'water').", + ) setup_parser.add_argument( "--force", action="store_true", @@ -94,6 +101,11 @@ def _command_parser(): help="Slater-Koster parameter set (default '3ob'). Selects the Hamiltonian " "parameters and matching spin constants.", ) + screen_parser.add_argument( + "--solvent", default=None, + help="GBSA/ALPB implicit-solvation solvent (e.g. 'water') applied to every " + "molecule. Default gas phase. Install with 'setup-dftb --solvent '.", + ) return parser @@ -123,9 +135,19 @@ def parse_args(argv=None): def run_setup_dftb(parser_args): """ - Download the default DFTB+ Slater-Koster parameter set. + Download a DFTB+ Slater-Koster parameter set, or a GBSA solvent parameter. """ + if parser_args.solvent is not None: + param_file = install_gbsa_param( + parser_args.solvent, + install_root=parser_args.install_root, + url=parser_args.url, + force=parser_args.force, + ) + print("GBSA solvation parameters:", param_file) + return 0 + parameter_dir = install_slakos( install_root=parser_args.install_root, url=parser_args.url, @@ -164,6 +186,7 @@ def run_screen(parser_args): pressure=parser_args.pressure, directory=parser_args.directory, parameter_set=parser_args.parameter_set, + solvent=parser_args.solvent, ) failed = sum(1 for record in results if record["status"] != "ok") diff --git a/ThermoScreening/thermo/api.py b/ThermoScreening/thermo/api.py index c787bbb..fa04178 100644 --- a/ThermoScreening/thermo/api.py +++ b/ThermoScreening/thermo/api.py @@ -18,7 +18,7 @@ from .thermo import Thermo from .atoms import Atom from ..calculator import Geoopt, Hessian, Modes -from ..calculator.dftbplus import _spin_kwargs, SPIN_CONSTANTS_3OB +from ..calculator.dftbplus import _spin_kwargs, _solvation_kwargs, SPIN_CONSTANTS_3OB logger = logging.getLogger(__package_name__).getChild("api") @@ -505,6 +505,8 @@ def dftbplus_thermo( directory=None, spin=None, spin_constants=None, + solvent=None, + solvation_param_file=None, **kwargs ): """ @@ -534,6 +536,14 @@ def dftbplus_thermo( Element -> spin-constant mapping matching the Slater-Koster set in use. Defaults to the 3ob constants; pass ``SPIN_CONSTANTS_MIO`` (or the value from :func:`resolve_parameter_set`) when running the mio set. + solvent : str, optional + Solvent name (e.g. ``"water"``) for GBSA/ALPB implicit solvation. When + set, the optimisation, energy, and Hessian all run in solution. Requires + the solvent's parameter file (``thermo setup-dftb --solvent ``). + Defaults to a gas-phase calculation. + solvation_param_file : str, optional + Explicit path to a GBSA parameter file, overriding ``solvent`` (use a + method-consistent set instead of the default GFN-fit one). Other Parameters ---------------- @@ -557,14 +567,19 @@ def dftbplus_thermo( spin_kwargs = _spin_kwargs(atoms, spin, spin_constants) + # Implicit solvation (empty for the gas-phase default). Applied to both the + # optimisation and the Hessian so the geometry and frequencies are consistent. + solvation_kwargs = _solvation_kwargs(solvent, solvation_param_file) + engine_kwargs = {**spin_kwargs, **solvation_kwargs, **kwargs} + with _run_in_directory(directory): # run geometry optimization - geoopt = Geoopt(atoms=atoms, charge=charge, **spin_kwargs, **kwargs) + geoopt = Geoopt(atoms=atoms, charge=charge, **engine_kwargs) potential_energy = geoopt.potential_energy() optimized_atoms = geoopt.read() # run hessian calculation - Hessian(atoms=optimized_atoms, charge=charge, **spin_kwargs, **kwargs) + Hessian(atoms=optimized_atoms, charge=charge, **engine_kwargs) # run normal mode calculation modes = Modes() diff --git a/ThermoScreening/thermo/screening.py b/ThermoScreening/thermo/screening.py index 06940d8..326b144 100644 --- a/ThermoScreening/thermo/screening.py +++ b/ThermoScreening/thermo/screening.py @@ -31,9 +31,11 @@ "path", "charge", "status", + "Eelec_hartree", "E_hartree", "H_hartree", "G_hartree", + "G_total_hartree", "S_cal_per_mol_K", "Cv_cal_per_mol_K", "error", @@ -98,10 +100,15 @@ def _load_jobs(source, charge: float, spin=None): def _thermo_summary(thermo): + # Eelec_hartree is the DFTB+ electronic (SCC) energy, which carries the + # implicit-solvation term; E/H/G_hartree are the thermal corrections and + # G_total_hartree = Eelec + G correction is the absolute Gibbs free energy. return { + "Eelec_hartree": thermo.electronic_energy(), "E_hartree": thermo.total_energy("H"), "H_hartree": thermo.total_enthalpy("H"), "G_hartree": thermo.total_gibbs_free_energy("H"), + "G_total_hartree": thermo.total_EeGtot(), "S_cal_per_mol_K": thermo.total_entropy("cal/(mol*K)"), "Cv_cal_per_mol_K": thermo.total_heat_capacity("cal/(mol*K)"), } @@ -135,6 +142,7 @@ def screen( parameters=None, spin=None, parameter_set="3ob", + solvent=None, ): """ Run a thermochemistry screen over a set of molecules. @@ -163,6 +171,10 @@ def screen( parameter_set : str Slater-Koster parameter set to use (``"3ob"`` or ``"mio"``). Selects both the default Hamiltonian parameters and the matching spin constants. + solvent : str, optional + Solvent name for GBSA/ALPB implicit solvation applied to every molecule + (e.g. ``"water"``). Defaults to gas phase. The solvent parameter file + must be installed (``thermo setup-dftb --solvent ``). Returns ------- @@ -194,6 +206,7 @@ def screen( directory=str(root / job.name), spin=job.spin, spin_constants=spin_constants, + solvent=solvent, **parameters, ) record.update(_thermo_summary(thermo)) diff --git a/tests/calculator/test_dftbplus.py b/tests/calculator/test_dftbplus.py index 85563dc..e51289d 100644 --- a/tests/calculator/test_dftbplus.py +++ b/tests/calculator/test_dftbplus.py @@ -448,6 +448,33 @@ def test_resolve_parameter_set_rejects_unknown_set(): dftbplus_module.resolve_parameter_set("does-not-exist") +def test_solvation_kwargs_gas_phase_is_empty(): + assert dftbplus_module._solvation_kwargs() == {} + assert dftbplus_module._solvation_kwargs(solvent=None, param_file=None) == {} + + +def test_solvation_kwargs_explicit_param_file(tmp_path): + param = tmp_path / "param_gbsa_h2o.txt" + param.write_text("data", encoding="utf-8") + + kw = dftbplus_module._solvation_kwargs(param_file=str(param)) + + assert kw["Hamiltonian_Solvation"] == "GeneralizedBorn {" + # DFTB+ resolves ParamFile relative to the run dir, so it must be absolute + assert os.path.isabs(kw["Hamiltonian_Solvation_ParamFile"]) + assert kw["Hamiltonian_Solvation_ParamFile"] == str(param.resolve()) + + +def test_solvation_kwargs_missing_param_file_raises(tmp_path): + with pytest.raises(FileNotFoundError): + dftbplus_module._solvation_kwargs(param_file=str(tmp_path / "nope.txt")) + + +def test_solvation_kwargs_missing_solvent_file_hints_setup(tmp_path): + with pytest.raises(FileNotFoundError, match="setup-dftb --solvent water"): + dftbplus_module._solvation_kwargs(solvent="water", install_root=tmp_path) + + @pytest.mark.skipif( not dftbplus_ready, reason="DFTB+ executables are not installed or cannot start.", diff --git a/tests/cli/test_dftb_setup.py b/tests/cli/test_dftb_setup.py index eea3c80..be27468 100644 --- a/tests/cli/test_dftb_setup.py +++ b/tests/cli/test_dftb_setup.py @@ -8,10 +8,13 @@ DEFAULT_PARAMETER_SET, REQUIRED_PARAMETER_FILE, Diagnostic, + _solvent_stem, check_dftb_setup, default_parameter_dir, dftb_prefix_export, format_diagnostics, + gbsa_param_path, + install_gbsa_param, install_slakos, slako_url, ) @@ -124,6 +127,60 @@ def test_install_slakos_rejects_unsafe_archive_member(tmp_path): assert not (tmp_path / "unsafe.txt").exists() +def test_solvent_stem_resolves_names_and_aliases(): + assert _solvent_stem("water") == "h2o" + assert _solvent_stem("Water") == "h2o" + assert _solvent_stem("dichloromethane") == "ch2cl2" + assert _solvent_stem("chloroform") == "chcl3" + + +def test_solvent_stem_rejects_unknown(): + with pytest.raises(ValueError, match="Unknown solvent"): + _solvent_stem("unobtainium") + + +def test_gbsa_param_path_uses_solvent_stem(tmp_path): + path = gbsa_param_path("water", install_root=tmp_path) + assert path.name == "param_gbsa_h2o.txt" + assert path.parent.name == "gfn2-0-1" + + +def test_install_gbsa_param_downloads_file(tmp_path): + source = tmp_path / "param_gbsa_h2o.txt" + source.write_text("solvent parameters", encoding="utf-8") + + installed = install_gbsa_param( + "water", install_root=tmp_path / "install", url=source.as_uri() + ) + + assert installed == gbsa_param_path("water", tmp_path / "install").resolve() + assert installed.read_text(encoding="utf-8") == "solvent parameters" + + +def test_install_gbsa_param_requires_downloaded_file(monkeypatch, tmp_path): + # a "download" that writes nothing must not silently succeed + monkeypatch.setattr(dftb_setup, "_download_file", lambda url, destination: None) + + with pytest.raises(FileNotFoundError): + install_gbsa_param("water", install_root=tmp_path / "install") + + +def test_install_gbsa_param_reuses_existing(monkeypatch, tmp_path): + destination = gbsa_param_path("water", tmp_path / "install") + destination.parent.mkdir(parents=True) + destination.write_text("existing", encoding="utf-8") + + def fail_download(*args, **kwargs): + raise AssertionError("download should not run") + + monkeypatch.setattr(dftb_setup, "_download_file", fail_download) + + installed = install_gbsa_param("water", install_root=tmp_path / "install") + + assert installed == destination.resolve() + assert destination.read_text(encoding="utf-8") == "existing" + + def test_dftb_prefix_export_adds_trailing_separator(tmp_path): expected = f'export DFTB_PREFIX="{tmp_path.resolve()}/"' diff --git a/tests/thermo/test_api.py b/tests/thermo/test_api.py index 0987def..09645b2 100644 --- a/tests/thermo/test_api.py +++ b/tests/thermo/test_api.py @@ -471,5 +471,32 @@ def test_dftbplus_thermo_uses_given_spin_constants(monkeypatch, tmp_path): assert seen["hessian_kwargs"]["Hamiltonian_SpinConstants_O"] == "{ -0.099 }" +def test_dftbplus_thermo_injects_solvation_into_both_steps(monkeypatch, tmp_path): + api, seen = _mock_pipeline(monkeypatch) + param = tmp_path / "param_gbsa_h2o.txt" + param.write_text("data", encoding="utf-8") + + api.dftbplus_thermo( + Atoms("OH2", positions=[[0, 0, 0.12], [0, 0.76, -0.48], [0, -0.76, -0.48]]), + directory=str(tmp_path / "j"), + solvation_param_file=str(param), + ) + + # the optimisation and the Hessian both run in solvent (consistent geometry + # and frequencies) + assert seen["geoopt_kwargs"]["Hamiltonian_Solvation"] == "GeneralizedBorn {" + assert seen["hessian_kwargs"]["Hamiltonian_Solvation"] == "GeneralizedBorn {" + + +def test_dftbplus_thermo_gas_phase_has_no_solvation(monkeypatch, tmp_path): + api, seen = _mock_pipeline(monkeypatch) + api.dftbplus_thermo( + Atoms("OH2", positions=[[0, 0, 0.12], [0, 0.76, -0.48], [0, -0.76, -0.48]]), + directory=str(tmp_path / "j"), + ) + + assert "Hamiltonian_Solvation" not in seen["geoopt_kwargs"] + + if __name__ == "__main__": unittest.main() diff --git a/tests/thermo/test_main.py b/tests/thermo/test_main.py index 321ca0b..0a4c3e5 100644 --- a/tests/thermo/test_main.py +++ b/tests/thermo/test_main.py @@ -114,6 +114,7 @@ def test_main_runs_setup_dftb(monkeypatch, tmp_path, capsys): url="file:///tmp/3ob-3-1.tar.xz", force=True, parameter_set="3ob", + solvent=None, ), ) monkeypatch.setattr( @@ -129,6 +130,29 @@ def test_main_runs_setup_dftb(monkeypatch, tmp_path, capsys): assert "export DFTB_PREFIX=" in output +def test_main_setup_dftb_downloads_solvent(monkeypatch, tmp_path, capsys): + monkeypatch.setattr( + thermo, + "parse_args", + lambda: argparse.Namespace( + command="setup-dftb", + install_root=str(tmp_path), + url=None, + force=False, + parameter_set="3ob", + solvent="water", + ), + ) + monkeypatch.setattr( + thermo, + "install_gbsa_param", + lambda solvent, install_root, url, force: tmp_path / "param_gbsa_h2o.txt", + ) + + assert thermo.main() == 0 + assert "GBSA solvation parameters:" in capsys.readouterr().out + + def test_main_runs_doctor(monkeypatch, capsys): diagnostic = type("DiagnosticStub", (), {"ok": False})() diff --git a/tests/thermo/test_screening.py b/tests/thermo/test_screening.py index bc47d79..9a1a610 100644 --- a/tests/thermo/test_screening.py +++ b/tests/thermo/test_screening.py @@ -10,6 +10,9 @@ class _FakeThermo: + def electronic_energy(self): + return -100.0 + def total_energy(self, unit): return -10.0 @@ -19,6 +22,9 @@ def total_enthalpy(self, unit): def total_gibbs_free_energy(self, unit): return -11.0 + def total_EeGtot(self): + return -111.0 + def total_entropy(self, unit): return 50.0 @@ -120,6 +126,26 @@ def test_screen_rejects_unknown_parameter_set(tmp_path): screening.screen(str(tmp_path), out=str(tmp_path / "r"), parameter_set="nope") +def test_screen_passes_solvent_to_dftbplus_thermo(monkeypatch, tmp_path): + _write_xyz(tmp_path / "mol.xyz") + + captured = {} + + def fake_thermo(atoms, solvent=None, **kwargs): + captured["solvent"] = solvent + return _FakeThermo() + + monkeypatch.setattr(screening, "dftbplus_thermo", fake_thermo) + screening.screen( + str(tmp_path), + out=str(tmp_path / "r"), + directory=str(tmp_path / "runs"), + solvent="water", + ) + + assert captured["solvent"] == "water" + + def test_load_jobs_rejects_unknown_source(tmp_path): bad = tmp_path / "thing.txt" bad.write_text("x", encoding="utf-8") @@ -173,6 +199,9 @@ def fake_thermo(atoms, charge=0, directory=None, **kwargs): assert [record["status"] for record in results] == ["ok", "error"] assert results[1]["error"] == "kaboom" assert results[0]["G_hartree"] == -11.0 + # electronic energy (carries solvation) and absolute Gibbs are reported too + assert results[0]["Eelec_hartree"] == -100.0 + assert results[0]["G_total_hartree"] == -111.0 # each molecule ran in its own directory assert str(tmp_path / "runs" / "good") in captured["dirs"] @@ -217,10 +246,14 @@ def test_cli_parse_args_routes_screen(): assert args.charge == -1.0 assert args.temperature == 300.0 assert args.parameter_set == "3ob" # default set + assert args.solvent is None # gas phase by default mio_args = cli.parse_args(["screen", "molecules.csv", "--parameter-set", "mio"]) assert mio_args.parameter_set == "mio" + solv_args = cli.parse_args(["screen", "molecules.csv", "--solvent", "water"]) + assert solv_args.solvent == "water" + def test_cli_run_screen_returns_failure_count(monkeypatch): import ThermoScreening.cli.thermo as cli @@ -232,6 +265,7 @@ def test_cli_run_screen_returns_failure_count(monkeypatch): args = Namespace( source="x", out="res", charge=0.0, temperature=298.15, pressure=101325.0, directory="screening", parameter_set="3ob", + solvent=None, ) assert cli.run_screen(args) == 1 # one molecule failed