diff --git a/fanpy/scripts/gaussian/run_calc.py b/fanpy/scripts/gaussian/run_calc.py index 6cee7780..f04a809f 100644 --- a/fanpy/scripts/gaussian/run_calc.py +++ b/fanpy/scripts/gaussian/run_calc.py @@ -8,11 +8,11 @@ def run_calc( one_int_file, two_int_file, wfn_type, - nuc_nuc=None, + nuc_nuc=0.0, optimize_orbs=False, - pspace_exc=None, - objective=None, - solver=None, + pspace_exc=(1, 2), + objective="projected", + solver="least_squares", solver_kwargs=None, wfn_kwargs=None, ham_noise=None, diff --git a/fanpy/scripts/pyscf/make_fanci_script.py b/fanpy/scripts/pyscf/make_fanci_script.py index 25237dfa..3cdafa87 100644 --- a/fanpy/scripts/pyscf/make_fanci_script.py +++ b/fanpy/scripts/pyscf/make_fanci_script.py @@ -571,6 +571,10 @@ def main(): # pragma: no cover help="Name of the file that contains the output of the script.", ) args = parser.parse_args() + # format pspace excitations + if args.pspace_exc is not None and type(args.pspace_exc) is str: + args.pspace_exc = args.pspace_exc.strip("[]") + args.pspace_exc = [int(x) for x in args.pspace_exc.split(",")] make_script( args.geom, args.wfn_type, diff --git a/fanpy/scripts/pyscf/pyscf_parser.py b/fanpy/scripts/pyscf/pyscf_parser.py index 8b3f3736..067eea85 100644 --- a/fanpy/scripts/pyscf/pyscf_parser.py +++ b/fanpy/scripts/pyscf/pyscf_parser.py @@ -22,13 +22,15 @@ default="Angstrom", help="Units for the geometry in the Hartree-Fock calculation. Default is 'angstrom'.") -parser.add_argument("--optimize_orbs", - type=bool, - default=False, - help="Whether to optimize orbitals in the Hartree-Fock calculation.") +parser.add_argument( + "--optimize_orbs", + action="store_true", + required=False, + help="Flag for optimizing orbitals. Orbitals are not optimized by default.", +) parser.add_argument("--pspace_exc", - type=list, + type=str, default=(1, 2), help="Excitations to include in the pspace. E.g. '(1, 2)'") diff --git a/fanpy/scripts/pyscf/run_calc.py b/fanpy/scripts/pyscf/run_calc.py index 9c45be21..a69dea77 100644 --- a/fanpy/scripts/pyscf/run_calc.py +++ b/fanpy/scripts/pyscf/run_calc.py @@ -78,12 +78,12 @@ def make_wfn_dirs(pattern: str, wfn_name: str, num_runs: int, rep_dirname_prefix except FileExistsError: pass -def write_wfn_py(pattern: str, wfn_type: str, optimize_orbs: bool=False, - pspace_exc=None, nproj=0, objective=None, solver=None, +def write_wfn_py(pattern: str, wfn_type: str, geom: list, basis: str, optimize_orbs: bool=False, + pspace_exc=None, objective=None, solver=None, ham_noise=None, wfn_noise=None, solver_kwargs=None, wfn_kwargs=None, load_orbs=None, load_ham=None, load_wfn=None, load_chk=None, load_prev=False, - memory=None, filename=None, ncores=1, exclude=None, old_fanpy=False): + memory=None, filename=None, ncores=1, exclude=None, fanpy_only=False): """Make a script for running calculations. @@ -100,6 +100,10 @@ def write_wfn_py(pattern: str, wfn_type: str, optimize_orbs: bool=False, "basecc", "standardcc", "generalizedcc", "senioritycc", "pccd", "ccsd", "ccsdt", "ccsdtq", "ap1rogsd", "ap1rogsd_spin", "apsetgd", "apsetgsd", "apg1rod", "apg1rosd", "ccsdsen0", "ccsdqsen0", "ccsdtqsen0", "ccsdtsen2qsen0". + geom : list + List of atomic coordinates for PySCF. + basis : str + Basis set for PySCF. optimize_orbs : bool If True, orbitals are optimized. If False, orbitals are not optimized. @@ -109,10 +113,6 @@ def write_wfn_py(pattern: str, wfn_type: str, optimize_orbs: bool=False, Orders of excitations that will be used to build the projection space. Default is first, second, third, and fourth order excitations of the HF ground state. Used for slower fanpy (i.e. `old_fanpy=True`) - nproj : int - Number of projection states that will be used. - Default uses all possible projection states (i.e. Slater determinants. - Used for faster fanpy (i.e. `old_fanpy=False`) objective : str Form of the Schrodinger equation that will be solved. Use `system` to solve the Schrodinger equation as a system of equations. @@ -171,9 +171,9 @@ def write_wfn_py(pattern: str, wfn_type: str, optimize_orbs: bool=False, filename : str Filename to save the generated script file. Default just prints it out to stdout. - old_fanpy : bool - Use old, slower (but probably more robust) fanpy. - Default uses faster fanpy. + fanpy_only : bool + Use fanpy only, this is slower (but probably more robust). + Default uses faster fanpy with the PyCI interface. Some features are not avaialble on new fanpy. """ @@ -186,7 +186,6 @@ def write_wfn_py(pattern: str, wfn_type: str, optimize_orbs: bool=False, if pspace_exc is None: pspace_exc = [1, 2, 3, 4] - pspace_exc = [str(i) for i in pspace_exc] if objective is None: objective = 'variational' @@ -245,18 +244,21 @@ def write_wfn_py(pattern: str, wfn_type: str, optimize_orbs: bool=False, save_chk = 'checkpoint.npy' - if old_fanpy: - pspace = ['--pspace', *pspace_exc] - else: - pspace = ['--nproj', str(nproj)] - subprocess.run(['fanpy_make_pyscf_script' if old_fanpy else 'fanpy_make_fanci_pyscf_script', + # convert geom list into json string with no space + geom_str = str(geom).replace(' ', '') + # convert to json style for fanpy + geom_str = geom_str.replace("'", '"') + + subprocess.run(['fanpy_make_pyscf_script' if fanpy_only else 'fanpy_make_fanci_pyscf_script', *optimize_orbs, '--wfn_type', wfn_type, + '--geom', geom_str, + '--pspace_exc', str(pspace_exc), + '--basis', basis, '--objective', objective, '--solver', solver, *kwargs, *load_files, '--save_chk', save_chk, '--filename', filename, *memory, - *pspace ]) os.chdir(cwd) @@ -320,9 +322,10 @@ def run_calcs(pattern: str, time=None, memory=None, ncores=1, outfile='outfile', f.write('cwd=$PWD\n') if calc_range: f.write(f'for i in {{{calc_range[0]}..{calc_range[1]}}}; do\n') + f.write(' cd _$i\n') else: f.write('for i in */; do\n') - f.write(' cd $i\n') + f.write(' cd $i\n') f.write(f' python -u ../calculate.py > {results_out}\n') f.write(' cd $cwd\n') f.write('done\n') diff --git a/tests/test_scripts_gaussian_run_calc.py b/tests/test_scripts_gaussian_run_calc.py new file mode 100644 index 00000000..b378dca4 --- /dev/null +++ b/tests/test_scripts_gaussian_run_calc.py @@ -0,0 +1,25 @@ +from fanpy.scripts.gaussian.run_calc import run_calc +from utils import find_datafile +import subprocess + +def test_run_calc(): + oneint = find_datafile("data/data_h2_hf_sto6g_oneint.npy") + twoint = find_datafile("data/data_h2_hf_sto6g_twoint.npy") + # attempt to run simple calculation with minimal inputs + # this checks if the default parameters are working correctly + run_calc( + nelec=2, + one_int_file=oneint, + two_int_file=twoint, + wfn_type="cisd") + +def test_run_calc_cli(): + oneint = find_datafile("data/data_h2_hf_sto6g_oneint.npy") + twoint = find_datafile("data/data_h2_hf_sto6g_twoint.npy") + subprocess.check_output([ + "fanpy_run_calc", + "--nelec", "2", + "--one_int_file", oneint, + "--two_int_file", twoint, + "--wfn_type", "cisd", + ]) \ No newline at end of file diff --git a/tests/test_scripts_pyscf_run_calc.py b/tests/test_scripts_pyscf_run_calc.py new file mode 100644 index 00000000..b1ff548b --- /dev/null +++ b/tests/test_scripts_pyscf_run_calc.py @@ -0,0 +1,147 @@ +import os +import subprocess +from pathlib import Path + +import pytest + +from fanpy.scripts.pyscf import run_calc + + +def test_write_pyscf_py_creates_file(tmp_path, monkeypatch): + # isolate make_pyscf_input to avoid heavy dependencies + monkeypatch.setattr(run_calc, "make_pyscf_input", lambda coords, **kwargs: f"COORDS: {coords}") + + monkeypatch.chdir(tmp_path) + coords = "H 0 0 0; H 0 0 0.74" + run_calc.write_pyscf_py("inpdir", coords, basis="sto-3g", memory="1GB", charge=0, spin=0, units="B") + + out_file = tmp_path / "inpdir" / "calculate.py" + assert out_file.exists() + content = out_file.read_text() + assert "COORDS" in content + assert "H 0 0 0.74" in content + +def test_write_pyscf_py_errors(): + # if memory not in MB or GB + with pytest.raises(ValueError): + run_calc.write_pyscf_py("inpdir", "H 0 0 0; H 0 0 0.74", basis="sto-3g", memory="1MQ") + +def test_make_wfn_dirs_creates_dirs(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + parent = "parent_dir" + wfn_name = "mywfn" + run_calc.make_wfn_dirs(parent, wfn_name, num_runs=3, rep_dirname_prefix="rep") + + for i in range(3): + d = tmp_path / parent / wfn_name / f"rep_{i}" + assert d.is_dir() + +# tests for write wfn py + +def test_write_wfn_py_creates_file(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + parent = "parent_dir" + wfn_name = "cisd" + h2_geom = [['H', ['0.0000000000', '0.0000000000', '0.7500000000']], + ['H', ['0.0000000000', '0.0000000000', '0']]] + basis = 'sto3g' + # create parent directory to run calculation in + parent_dir = tmp_path / parent + parent_dir.mkdir(parents=True) + + run_calc.write_wfn_py(parent, wfn_name, h2_geom, basis, objective='projected', solver='least_squares') + + out_file = parent_dir / "calculate.py" + assert out_file.exists() + content = out_file.read_text() + assert "CISD" in content # this should be in generated file + assert "sto3g" in content + assert "ProjectedSchrodinger" + +def test_write_wfn_py_kwargs(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + parent = "parent_dir" + wfn_name = "cisd" + h2_geom = [['H', ['0.0000000000', '0.0000000000', '0.7500000000']], + ['H', ['0.0000000000', '0.0000000000', '0']]] + basis = 'sto3g' + # create parent directory to run calculation in + parent_dir = tmp_path / parent + parent_dir.mkdir(parents=True) + run_calc.write_wfn_py(parent, wfn_name, h2_geom, basis, objective='projected', solver='least_squares', wfn_noise=1e-5, ham_noise=1e-6, pspace_exc=[1,2], memory='2gb') + out_file = parent_dir / "calculate.py" + assert out_file.exists() + content = out_file.read_text() + assert "wfn.params + 1e-05" in content + assert "ham.params + 1e-06" in content + subprocess.run(["python", str(out_file)], check=True) + + +def test_run_calcs(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + # create database folder structure. This is necessary for run_calcs to work + db_dir = os.path.join(tmp_path, "database") + os.makedirs(db_dir, exist_ok=True) + basis = "sto-3g" + basis_dir = os.path.join(db_dir, "H2", basis) + os.makedirs(basis_dir, exist_ok=True) + + # create wfn dirs + run_calc.make_wfn_dirs(basis_dir, "cisd", num_runs=3) + wfn_dir = os.path.join(basis_dir, "cisd") + + # create a dummy calculate.py that just writes to output.txt + calc_file = os.path.join(wfn_dir, "calculate.py") + with open(calc_file, 'w') as f: + f.write("with open('output.txt', 'w') as f: f.write('Calculation complete')") + + # check if file exists + run_calc.run_calcs(calc_file, calc_range=[0,2]) + output_file = os.path.join(wfn_dir, "_0", "output.txt") + assert os.path.isfile(output_file) + # check if content is correct + with open(output_file, 'r') as f: + content = f.read() + assert content == "Calculation complete" + + # run calcs without calc_range: + # create wfn dirs + run_calc.make_wfn_dirs(basis_dir, "ccsd", num_runs=1) + wfn_dir = os.path.join(basis_dir, "ccsd") + calc_file = os.path.join(wfn_dir, "calculate.py") + with open(calc_file, 'w') as f: + f.write("with open('output.txt', 'w') as f: f.write('Calculation complete')") + run_calc.run_calcs(calc_file) + output_file = os.path.join(wfn_dir, "_0", "output.txt") + assert os.path.isfile(output_file) + # check if content is correct + with open(output_file, 'r') as f: + content = f.read() + assert content == "Calculation complete" + +def test_run_calcs_errors(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + # create a dummy calculate.py that just writes to output.txt + calc_file = os.path.join(tmp_path, "calculate.py") + with open(calc_file, 'w') as f: + f.write("with open('output.txt', 'w') as f: f.write('Calculation complete')") + + # need to provide both time and memory + with pytest.raises(ValueError) as excinfo: + run_calc.run_calcs(calc_file, memory='2gb') + assert str(excinfo.value) == "You cannot provide only one of the time and memory." + + with pytest.raises(ValueError) as excinfo: + run_calc.run_calcs(calc_file, time='1h') + assert str(excinfo.value) == "You cannot provide only one of the time and memory." + + # wrong time unit + with pytest.raises(ValueError) as excinfo: + run_calc.run_calcs(calc_file, time='1p', memory='2gb') + assert str(excinfo.value) == "Time must be given in minutes, hours, or days (e.g. 1440m, 24h, 1d)." + + # wrong memory unit + with pytest.raises(ValueError) as excinfo: + run_calc.run_calcs(calc_file, time='1h', memory='4KB') + assert str(excinfo.value) == "Memory must be given as a MB or GB (e.g. 1024MB, 1GB)" \ No newline at end of file