diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdd945d..eb5c8d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt - name: Run tests - run: coverage run setup.py test + run: coverage run -m pytest - name: Verify Coverage if: github.event_name == 'push' uses: codecov/codecov-action@v1.5.2 diff --git a/ccinput/constants.py b/ccinput/constants.py index 546fd88..9f68268 100644 --- a/ccinput/constants.py +++ b/ccinput/constants.py @@ -142,7 +142,16 @@ class CalcType(Enum): "hf": ["hartreefock"], # Some old exotic semi-empirical methods in ORCA are missing "semiempirical": ["am1", "pm3", "pm6", "pm7", "mndo"], - "xtb": ["gfn2xtb", "xtb2", "xtb1", "gfn1xtb", "gfn0xtb", "gfnff", "gfn2xtbgfnff"], + "xtb": [ + "gfn2xtb", + "xtb2", + "xtb1", + "gfn1xtb", + "gfn0xtb", + "gfnff", + "gfn2xtbgfnff", + "gxtb", + ], "special": ["hf3c", "pbeh3c", "r2scan3c", "b973c"], "mp2": ["mp2", "rimp2", "direct_mp2"], "cc": [ @@ -171,6 +180,7 @@ class CalcType(Enum): "xtb": [], "psi4": ["psi4"], "nwchem": [], + "pyscf": [], } # Synonym for each solvent. @@ -1696,6 +1706,7 @@ class CalcType(Enum): "gfn0xtb": "gfn0-xtb", "gfnff": "gfn-ff", "gfn2xtbgfnff": "gfn2-xtb//gfn-ff", + "gxtb": "gxtb", }, "psi4": { "hf": "scf", diff --git a/ccinput/packages/nwchem.py b/ccinput/packages/nwchem.py index 382a239..4d90eeb 100644 --- a/ccinput/packages/nwchem.py +++ b/ccinput/packages/nwchem.py @@ -254,7 +254,7 @@ def add_option(self, key, option): # User has to put some of the following keyword as specification, independant of what calculation was specified in input if key in [ "string", - "freezing string sethod", + "freezing string method", "fsm", "freezing string", ]: diff --git a/ccinput/packages/orca.py b/ccinput/packages/orca.py index 1fa34a1..785dc63 100644 --- a/ccinput/packages/orca.py +++ b/ccinput/packages/orca.py @@ -189,9 +189,7 @@ def handle_command(self): MO("in-LUMO.cube",{n_LUMO},0); MO("in-LUMOA.cube",{n_LUMO1},0); MO("in-LUMOB.cube",{n_LUMO2},0); - """.split( - "\n" - ), + """.split("\n"), ) elif self.calc.type == CalcType.FREQ: self.command_line = "FREQ " @@ -429,9 +427,7 @@ def create_input_file(self): self.block_lines += """%{} {} end - """.format( - name, "\n".join(content).strip() - ) + """.format(name, "\n".join(content).strip()) self.block_lines += f"""%MaxCore {self.mem_per_core}""" diff --git a/ccinput/packages/pyscf.py b/ccinput/packages/pyscf.py new file mode 100644 index 0000000..ecd08ab --- /dev/null +++ b/ccinput/packages/pyscf.py @@ -0,0 +1,357 @@ +from ccinput.constants import ( + CalcType, + ATOMIC_NUMBER, + LOWERCASE_ATOMIC_SYMBOLS, + SOFTWARE_BASIS_SETS, +) +from ccinput.utilities import ( + get_method, + get_solvent, + clean_xyz, + warn, + parse_specifications, +) +from ccinput.exceptions import ( + InvalidParameter, + UnimplementedError, + ImpossibleCalculation, +) + + +class PySCFCalculation: + calc = None + + has_scan = False + pal = 0 + mem = 0 + blocks = [] + + TEMPLATE = """!{} + *xyz {} {} + {}* + {}""" + # Command Line + # Charge + # Multiplicity + # XYZ structure + # Options blocks + + command_line = "" + xyz_structure = "" + block_lines = "" + + input_file = "" + + CALC_TYPES = [ + CalcType.SP, + CalcType.OPT, + CalcType.CONSTR_OPT, + CalcType.FREQ, + CalcType.TS, + CalcType.OPTFREQ, + ] + + def __init__(self, calc): + self.calc = calc + self.has_scan = False + self.pal = 0 + self.mem = 0 + + ### + + self.mol_specifications = { + "verbose": 5, + } + + self.command_line = "" + self.output_elements = {} + self.mf_lines = [] + + self.additional_commands = "" + self.xyz_structure = "" + self.input_file = "" + + self.specifications = {} + self.specifications_lines = [] + self.import_lines = [] + + self.solvation_radii = {} + self.aux_basis_set = "" + + if self.calc.type not in self.CALC_TYPES: + raise ImpossibleCalculation( + f"PySCF does not support calculations of type {self.calc.type}" + ) + + self.handle_specifications() + + self.handle_command() + self.handle_xyz() + + self.handle_pal_mem() + self.parse_custom_solvation_radii() + self.handle_solvation() + + self.create_input_file() + + @property + def confirmed_specifications(self): + """Returns the effective additional commands; saved in the Parameters object""" + return self.additional_commands + + def clean(self, s): + WHITELIST = set( + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/()=-,. {}_[]" + ) + return "".join([c for c in s if c in WHITELIST]) + + def add_to_block(self, block, lines): + if block in self.blocks: + self.blocks[block] += lines + else: + self.blocks[block] = lines + + def add_option(self, key, option): + if option == "": + # Maybe not rigorously correct, but should do the expected thing + if key[-2:] == "/c": + self.aux_basis_set = key[:-2] + elif key[-3:] == "/jk": + self.aux_basis_set = key[:-3] + elif key[-2:] == "/j": + self.aux_basis_set = key[:-2] + elif key not in self.specifications_list: + self.specifications_list.append(key) + else: + try: + option = int(option) + except ValueError: + pass + self.mol_specifications[key] = option + + def handle_specifications(self): + _specifications = ( + self.clean(self.calc.parameters.specifications).lower().strip() + ) + + parse_specifications(_specifications, self.add_option, condense=False) + + if self.calc.parameters.d3: + raise Exception("Not implemented") + elif self.calc.parameters.d3bj: + raise Exception("Not implemented") + + def handle_command(self): + if self.calc.type == CalcType.OPT: + self.command_line = "OPT " + # imports + elif self.calc.type == CalcType.OPTFREQ: + self.command_line = "OPT FREQ " + # imports + elif self.calc.type == CalcType.TS: + self.command_line = """ + mf.kernel() + params = {'transition': True, 'trust': 0.02, 'tmax': 0.06, "hessian": "file:hessian.txt"} + mol_ts = mf.Gradients().optimizer(solver='geomeTRIC').kernel(params) + """.strip() + self.output_elements = { + "xyz": "mol_ts.tostring()", + } + elif self.calc.type == CalcType.FREQ: + self.command_line = """ + mf.kernel() + ohess = mf.Hessian() + hess = ohess.kernel() + res_vib = harmonic_analysis(mf.mol, hess, imaginary_freq=False) + res_thermo = thermo(mf, res_vib["freq_au"]) + """.strip() + self.import_lines.append( + "from pyscf.hessian.thermo import thermo, harmonic_analysis" + ) + self.output_elements = { + "SCF": "mf.e_tot", + "H": 'res_thermo["H_tot"][0]', + "G": 'res_thermo["G_tot"][0]', + "hessian": "hess.tolist()", + "freqs": 'res_vib["freq_wavenumber"].tolist()', + "modes": 'res_vib["norm_mode"].tolist()', + } + elif self.calc.type == CalcType.CONSTR_OPT: + ### + # imports + if len(self.calc.constraints) == 0: + raise InvalidParameter("No constraints for constrained optimisation") + + scans = [] + freeze = [] + + for constr in self.calc.constraints: + if constr.scan: + scans.append(constr.to_orca()) + else: + freeze.append(constr.to_orca()) + + if len(scans) > 0: + scan_block = """Scan + {} + end + """ + self.add_to_block( + "geom", scan_block.format("".join(scans).strip()).split("\n") + ) + + if len(freeze) > 0: + freeze_block = """Constraints + {} + end + """ + self.add_to_block( + "geom", freeze_block.format("".join(freeze).strip()).split("\n") + ) + elif self.calc.type == CalcType.SP: + self.command_line = "mf.kernel()" + self.output_elements = { + "SCF": "mf.e_tot", + } + + self.method = get_method(self.calc.parameters.method, "orca") #### pyscf + + # if DFT + + self.mf_lines = ['mf = dft.rks.RKS(mol, xc="{}")'.format(self.method)] + self.import_lines.append("from pyscf import dft") + + if self.aux_basis_set != "": + self.mf_lines.append( + f'mf = mf.density_fit(auxbasis="{self.aux_basis_set}")' + ) + + def handle_xyz(self): + lines = [i + "\n" for i in clean_xyz(self.calc.xyz).split("\n") if i != ""] + self.xyz_structure = "".join(lines) + + def handle_pal_mem(self): + self.pal = self.calc.nproc + self.mem_per_core = int(self.calc.mem / self.calc.nproc) + self.mem = self.calc.mem + + def parse_custom_solvation_radii(self): + for radius in self.calc.parameters.custom_solvation_radii.split(";"): + if radius.strip() == "": + continue + sradius = radius.split("=") + if len(sradius) != 2: + raise InvalidParameter( + f"Invalid custom solvation radius specification: '{radius}': must follow the pattern '=;...'" + ) + + element, rad = sradius + if element not in LOWERCASE_ATOMIC_SYMBOLS: + raise InvalidParameter( + f"Invalid element in custom solvation radius specifications: '{element}'" + ) + + _element = ATOMIC_NUMBER[LOWERCASE_ATOMIC_SYMBOLS[element]] + + try: + _rad = float(rad) + except ValueError: + raise InvalidParameter( + f"Invalid custom solvation radius for element {element}: '{rad}'" + ) + self.solvation_radii[_element] = _rad + + def handle_solvation(self): + return #### + + if self.calc.parameters.solvent.lower() not in ["vacuum", ""]: + solvent_keyword = get_solvent( + self.calc.parameters.solvent, + self.calc.parameters.software, + solvation_model=self.calc.parameters.solvation_model, + ) + model = self.calc.parameters.solvation_model + radii_set = self.calc.parameters.solvation_radii + custom_radii = self.calc.parameters.custom_solvation_radii + + if model == "smd": + # Refined solvation radii + # E. Engelage, N. Schulz, F. Heinen, S. M. Huber, D. G. Truhlar, + # C. J. Cramer, Chem. Eur. J. 2018, 24, 15983–15987. + if radii_set == "smd18": + if 53 not in self.solvation_radii: + self.solvation_radii[53] = 2.74 + if 35 not in self.solvation_radii: + self.solvation_radii[35] = 2.60 + + # self.add_to_block( + # "cpcm", ["smd true", f'SMDsolvent "{solvent_keyword}"', radii_specs] + # ) + else: + raise InvalidParameter( + f"Invalid solvation model for PySCF: " + f"'{self.calc.parameters.solvation_model}'" + ) + + def create_input_file(self): + self.block_lines = "" + + mol_options = 'atom="""{}""",\n'.format(self.xyz_structure.strip()) + mol_options += 'basis="{}",\n'.format(self.calc.parameters.basis_set) + mol_options += "charge={},\n".format(self.calc.charge) + mol_options += "spin={},\n".format(self.calc.multiplicity - 1) + + for k, v in self.mol_specifications.items(): + if isinstance(v, int): + mol_options += "{}={},\n".format(k, v) + else: + mol_options += '{}="{}",\n'.format(k, v) + + # from pyscf.geomopt.geometric_solver import optimize + + output = """ + import os, json, pyscf + from pyscf.lib import num_threads + {imports} + + os.environ["OMP_NUM_THREADS"] = "{pal}" + os.environ["MKL_NUM_THREADS"] = "{pal}" + os.environ["OPENBLAS_NUM_THREADS"] = "{pal}" + num_threads({pal}) + + mol = pyscf.gto.M( + {mol_options}) + + mol.max_memory = {mem} + {mf_line} + {specifications}{command_line} + print(json.dumps({{ + {output_elements} + }})) + """ + + extra_imports = "\n".join(self.import_lines) + + output_elements_str = "" + + for k, v in self.output_elements.items(): + output_elements_str += f'"{k}": {v},\n' + + raw = output.format( + imports=extra_imports, + pal=self.pal, + mol_options=mol_options, + mem=self.mem, + mf_line="\n".join(self.mf_lines), + specifications="\n".join(self.specifications_lines), + command_line=self.command_line, + output_elements=output_elements_str.strip(), + ) + + self.input_file = "\n".join([i.strip() for i in raw.split("\n")]).replace( + "\n\n\n", "\n\n" + ) + + @property + def output(self): + return self.input_file diff --git a/ccinput/packages/xtb.py b/ccinput/packages/xtb.py index 7c80bb8..ca4e0bb 100644 --- a/ccinput/packages/xtb.py +++ b/ccinput/packages/xtb.py @@ -23,6 +23,7 @@ class XtbCalculation: "gfn0-xtb": "--gfn 0 ", "gfn-ff": "--gfnff ", "gfn2-xtb//gfn-ff": "--gfn2gfnff ", + "gxtb": "--gxtb ", } def __init__(self, calc): @@ -181,7 +182,7 @@ def handle_specifications(self): continue ss = spec.strip().split() if len(ss) == 1: - if ss[0] in ["gfn2", "gfn1", "gfn0", "gfnff", "gfn2//gfnff"]: + if ss[0] in ["gfn2", "gfn1", "gfn0", "gfnff", "gfn2//gfnff", "gxtb"]: if ss[0] == "gfn2//gfnff" and self.calc.type not in [ CalcType.CONF_SEARCH, CalcType.CONSTR_CONF_SEARCH, diff --git a/ccinput/tests/test_pyscf.py b/ccinput/tests/test_pyscf.py new file mode 100644 index 0000000..f2630ac --- /dev/null +++ b/ccinput/tests/test_pyscf.py @@ -0,0 +1,367 @@ +from ccinput.tests.testing_utilities import InputTests +from ccinput.exceptions import InvalidParameter, ImpossibleCalculation + + +class PySCFTests(InputTests): + def test_sp(self): + params = { + "nproc": 8, + "type": "Single-Point Energy", + "file": "Cl.xyz", + "software": "PySCF", + "method": "M06-2X", + "basis_set": "Def2-SVP", + "charge": "-1", + } + + inp = self.generate_calculation(**params) + + REF = ''' + import os, json, pyscf + from pyscf.lib import num_threads + from pyscf import dft + + os.environ["OMP_NUM_THREADS"] = "8" + os.environ["MKL_NUM_THREADS"] = "8" + os.environ["OPENBLAS_NUM_THREADS"] = "8" + num_threads(8) + + mol = pyscf.gto.M( + atom="""Cl 0.0 0.0 0.0""", + basis="def2svp", + charge=-1, + spin=0, + verbose=5, + ) + + mol.max_memory = 1000 + mf = dft.rks.RKS(mol, xc="M062X") + mf.kernel() + print(json.dumps({ + "SCF": mf.e_tot, + }))''' + + self.assertTrue(self.is_equivalent(REF, inp.input_file)) + + def test_df(self): + params = { + "nproc": 8, + "type": "Single-Point Energy", + "file": "Cl.xyz", + "software": "PySCF", + "method": "M06-2X", + "basis_set": "Def2-SVP", + "charge": "-1", + "specifications": "def2svp/JK", + } + + inp = self.generate_calculation(**params) + + REF = ''' + import os, json, pyscf + from pyscf.lib import num_threads + from pyscf import dft + + os.environ["OMP_NUM_THREADS"] = "8" + os.environ["MKL_NUM_THREADS"] = "8" + os.environ["OPENBLAS_NUM_THREADS"] = "8" + num_threads(8) + + mol = pyscf.gto.M( + atom="""Cl 0.0 0.0 0.0""", + basis="def2svp", + charge=-1, + spin=0, + verbose=5, + ) + + mol.max_memory = 1000 + mf = dft.rks.RKS(mol, xc="M062X") + mf = mf.density_fit(auxbasis="def2svp") + mf.kernel() + print(json.dumps({ + "SCF": mf.e_tot, + }))''' + + self.assertTrue(self.is_equivalent(REF, inp.input_file)) + + def test_grid(self): + params = { + "nproc": 8, + "type": "Single-Point Energy", + "file": "Cl.xyz", + "software": "PySCF", + "method": "M06-2X", + "basis_set": "Def2-SVP", + "charge": "-1", + "specifications": "grid(5)", + } + + inp = self.generate_calculation(**params) + + REF = ''' + import os, json, pyscf + from pyscf.lib import num_threads + from pyscf import dft + + os.environ["OMP_NUM_THREADS"] = "8" + os.environ["MKL_NUM_THREADS"] = "8" + os.environ["OPENBLAS_NUM_THREADS"] = "8" + num_threads(8) + + mol = pyscf.gto.M( + atom="""Cl 0.0 0.0 0.0""", + basis="def2svp", + charge=-1, + spin=0, + verbose=5, + grid=5, + ) + + mol.max_memory = 1000 + mf = dft.rks.RKS(mol, xc="M062X") + mf.kernel() + print(json.dumps({ + "SCF": mf.e_tot, + }))''' + + self.assertTrue(self.is_equivalent(REF, inp.input_file)) + + def test_functional(self): + params = { + "nproc": 8, + "type": "Single-Point Energy", + "file": "Cl.xyz", + "software": "PySCF", + "method": "PBE0", + "basis_set": "Def2-SVP", + "charge": "-1", + } + + inp = self.generate_calculation(**params) + + REF = ''' + import os, json, pyscf + from pyscf.lib import num_threads + from pyscf import dft + + os.environ["OMP_NUM_THREADS"] = "8" + os.environ["MKL_NUM_THREADS"] = "8" + os.environ["OPENBLAS_NUM_THREADS"] = "8" + num_threads(8) + + mol = pyscf.gto.M( + atom="""Cl 0.0 0.0 0.0""", + basis="def2svp", + charge=-1, + spin=0, + verbose=5, + ) + + mol.max_memory = 1000 + mf = dft.rks.RKS(mol, xc="PBE0") + mf.kernel() + print(json.dumps({ + "SCF": mf.e_tot, + }))''' + + self.assertTrue(self.is_equivalent(REF, inp.input_file)) + + def test_nproc(self): + params = { + "nproc": 4, + "type": "Single-Point Energy", + "file": "Cl.xyz", + "software": "PySCF", + "method": "M06-2X", + "basis_set": "Def2-SVP", + "charge": "-1", + } + + inp = self.generate_calculation(**params) + + REF = ''' + import os, json, pyscf + from pyscf.lib import num_threads + from pyscf import dft + + os.environ["OMP_NUM_THREADS"] = "4" + os.environ["MKL_NUM_THREADS"] = "4" + os.environ["OPENBLAS_NUM_THREADS"] = "4" + num_threads(4) + + mol = pyscf.gto.M( + atom="""Cl 0.0 0.0 0.0""", + basis="def2svp", + charge=-1, + spin=0, + verbose=5, + ) + + mol.max_memory = 1000 + mf = dft.rks.RKS(mol, xc="M062X") + mf.kernel() + print(json.dumps({ + "SCF": mf.e_tot, + }))''' + + self.assertTrue(self.is_equivalent(REF, inp.input_file)) + + def test_mem(self): + params = { + "nproc": 8, + "type": "Single-Point Energy", + "file": "Cl.xyz", + "software": "PySCF", + "method": "M06-2X", + "basis_set": "Def2-SVP", + "charge": "-1", + "mem": "10G", + } + + inp = self.generate_calculation(**params) + + REF = ''' + import os, json, pyscf + from pyscf.lib import num_threads + from pyscf import dft + + os.environ["OMP_NUM_THREADS"] = "8" + os.environ["MKL_NUM_THREADS"] = "8" + os.environ["OPENBLAS_NUM_THREADS"] = "8" + num_threads(8) + + mol = pyscf.gto.M( + atom="""Cl 0.0 0.0 0.0""", + basis="def2svp", + charge=-1, + spin=0, + verbose=5, + ) + + mol.max_memory = 10000 + mf = dft.rks.RKS(mol, xc="M062X") + mf.kernel() + print(json.dumps({ + "SCF": mf.e_tot, + }))''' + + self.assertTrue(self.is_equivalent(REF, inp.input_file)) + + def test_freq(self): + params = { + "nproc": 8, + "type": "Frequency Calculation", + "file": "benzene.xyz", + "software": "PySCF", + "method": "M06-2X", + "basis_set": "Def2-SVP", + } + + inp = self.generate_calculation(**params) + + REF = ''' + import os, json, pyscf + from pyscf.lib import num_threads + from pyscf.hessian.thermo import thermo, harmonic_analysis + from pyscf import dft + + os.environ["OMP_NUM_THREADS"] = "8" + os.environ["MKL_NUM_THREADS"] = "8" + os.environ["OPENBLAS_NUM_THREADS"] = "8" + num_threads(8) + + mol = pyscf.gto.M( + atom="""C -0.99790 -1.58780 -0.02080 + C 0.39730 -1.58780 -0.02080 + C 1.09480 -0.38010 -0.02080 + C 0.39720 0.82840 -0.02200 + C -0.99760 0.82830 -0.02250 + C -1.69520 -0.37990 -0.02150 + H -1.54760 -2.54020 -0.02030 + H 0.94680 -2.54040 -0.01950 + H 2.19450 -0.38000 -0.02020 + H 0.94740 1.78060 -0.02200 + H -1.54780 1.78060 -0.02340 + H -2.79480 -0.37970 -0.02160""", + basis="def2svp", + charge=0, + spin=0, + verbose=5, + ) + + mol.max_memory = 1000 + mf = dft.rks.RKS(mol, xc="M062X") + mf.kernel() + ohess = mf.Hessian() + hess = ohess.kernel() + res_vib = harmonic_analysis(mf.mol, hess, imaginary_freq=False) + res_thermo = thermo(mf, res_vib["freq_au"]) + print(json.dumps({ + "SCF": mf.e_tot, + "H": res_thermo["H_tot"][0], + "G": res_thermo["G_tot"][0], + "hessian": hess.tolist(), + "freqs": res_vib["freq_wavenumber"].tolist(), + "modes": res_vib["norm_mode"].tolist(), + }))''' + + self.assertTrue(self.is_equivalent(REF, inp.input_file)) + + def test_ts(self): + params = { + "nproc": 8, + "type": "TS Optimization", + "file": "small_ts.xyz", + "software": "PySCF", + "method": "M06-2X", + "basis_set": "Def2-SVP", + "specifications": "verbose=4", + } + + inp = self.generate_calculation(**params) + + REF = ''' + import os, json, pyscf + from pyscf.lib import num_threads + from pyscf import dft + + os.environ["OMP_NUM_THREADS"] = "8" + os.environ["MKL_NUM_THREADS"] = "8" + os.environ["OPENBLAS_NUM_THREADS"] = "8" + num_threads(8) + + mol = pyscf.gto.M( + atom="""C -1.30199679200336 0.23770149604323 -0.21903830900536 + C -0.65452549088797 1.37677004974294 0.23784083906211 + H -0.89567737188508 2.31097104509050 -0.24568023160592 + H -0.51131365003345 1.45158760940331 1.30393146403304 + O -0.98734706501576 -0.93896491644235 0.24464647161953 + C 0.57938721313073 -1.21542290559438 0.60370912581272 + C 1.30373709071931 -0.40352018279841 -0.31679957603999 + C 1.21326659501348 0.97578532623407 -0.11796726468775 + H 1.54268192054322 1.35508505207923 0.83816378823672 + H 1.50364201896930 1.60996020761999 -0.94605411105798 + H 1.33180817109859 -0.76458664777933 -1.33953946285788 + H 0.61967054666194 -2.29023171829519 0.47066760098394 + H 0.64159071923251 -0.92315930061295 1.64911403264797 + H -1.77094330174335 0.23668457701018 -1.20684978624530""", + basis="def2svp", + charge=0, + spin=0, + verbose=4, + ) + + mol.max_memory = 1000 + mf = dft.rks.RKS(mol, xc="M062X") + mf.kernel() + params = {'transition': True, 'trust': 0.02, 'tmax': 0.06, "hessian": "file:hessian.txt"} + mol_ts = mf.Gradients().optimizer(solver='geomeTRIC').kernel(params) + print(json.dumps({ + "xyz": mol_ts.tostring(), + })) + ''' + self.assertTrue(self.is_equivalent(REF, inp.input_file)) + + +# grid level diff --git a/ccinput/tests/test_xtb.py b/ccinput/tests/test_xtb.py index c41ff23..69008d7 100644 --- a/ccinput/tests/test_xtb.py +++ b/ccinput/tests/test_xtb.py @@ -838,6 +838,34 @@ def test_gfnff_method(self): self.assertTrue(self.is_equivalent(REF, xtb.command)) + def test_gxtb(self): + params = { + "type": "Single-Point Energy", + "file": "ethanol.xyz", + "software": "xtb", + "specifications": "--gxtb", + } + + xtb = self.generate_calculation(**params) + + REF = "xtb ethanol.xyz --gxtb" + + self.assertTrue(self.is_equivalent(REF, xtb.command)) + + def test_gxtb_method(self): + params = { + "type": "Single-Point Energy", + "file": "ethanol.xyz", + "software": "xtb", + "method": "gxtb", + } + + xtb = self.generate_calculation(**params) + + REF = "xtb ethanol.xyz --gxtb" + + self.assertTrue(self.is_equivalent(REF, xtb.command)) + def test_unavailable_calc_type(self): params = { "type": "TS Optimisation", diff --git a/ccinput/tests/testing_utilities.py b/ccinput/tests/testing_utilities.py index bdc48d2..5c8e530 100644 --- a/ccinput/tests/testing_utilities.py +++ b/ccinput/tests/testing_utilities.py @@ -55,8 +55,19 @@ def is_equivalent(self, ref, res): return True def xyz_line_equivalent(self, line1, line2): - sline1 = line1.strip().split() - sline2 = line2.strip().split() + + sline1 = line1.strip() + sline2 = line2.strip() + + if 'atom="""' in sline1: + sline1 = sline1[8:] + sline2 = sline2[8:] + if '""",' in sline1: + sline1 = sline1[:-4] + sline2 = sline2[:-4] + + sline1 = sline1.split() + sline2 = sline2.split() if len(sline1) != 4 or len(sline2) != 4: return False diff --git a/ccinput/utilities.py b/ccinput/utilities.py index 822655e..701f70d 100644 --- a/ccinput/utilities.py +++ b/ccinput/utilities.py @@ -282,6 +282,10 @@ def is_exchange_correlation_combination(method, software): def get_method(method, software): + if software == "pyscf": + # PySCF already handles synonyms to some extent + return method + try: abs_method = get_abs_method(method) except InvalidParameter: @@ -336,6 +340,10 @@ def get_method(method, software): def get_basis_set(basis_set, software): + if software == "pyscf": + # PySCF already handles synonyms to some extent + return basis_set + try: abs_basis_set = get_abs_basis_set(basis_set) except InvalidParameter: diff --git a/ccinput/wrapper.py b/ccinput/wrapper.py index 55ae3c4..c7b668a 100644 --- a/ccinput/wrapper.py +++ b/ccinput/wrapper.py @@ -10,6 +10,7 @@ from ccinput.packages.xtb import XtbCalculation from ccinput.drivers.pysis import PysisDriver from ccinput.packages.psi4 import Psi4Calculation +from ccinput.packages.pyscf import PySCFCalculation from ccinput.calculation import ( Calculation, @@ -44,6 +45,7 @@ "pysisyphus": PysisDriver, "qchem": QChemCalculation, "psi4": Psi4Calculation, + "pyscf": PySCFCalculation, }