diff --git a/HISTORY.rst b/HISTORY.rst index 87497c4..a05ad52 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,10 @@ ======= History ======= +2024.1.18 -- Support for running in containers and writing input only. + * Added new property: scaled dipole. + * Added option to write the input file and not run DFTB+ + 2023.11.10 -- Standard structure handling and cleaned up output * Switched to standard structure handling and naming, giving consistent options across SEAMM. @@ -15,7 +19,7 @@ History 2023.11.7 -- Added structure to orbital and density plots * The Dashboard expects 'structure.sdf' in order to display the structure with the - orbital or debsity plots from CUBE files. + orbital or density plots from CUBE files. 2023.3.5 -- Fixed issues with bandstructure and DOS * The bandstructure and DOS substeps updated to work with changes in the underlying diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 81264af..90bba7d 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -8,7 +8,6 @@ dependencies: # molsystem requires, and only available via Conda. - hsd-python - - psutil - seamm - seamm-util - tabulate @@ -27,6 +26,7 @@ dependencies: # Pip-only installs - pip: - cms_plots + - seamm-exec # Documentation - sphinx-copybutton - sphinxnotes-strike diff --git a/dftbplus_step/base.py b/dftbplus_step/base.py index 0b58b21..b27813d 100644 --- a/dftbplus_step/base.py +++ b/dftbplus_step/base.py @@ -3,23 +3,22 @@ """Non-graphical part of the DFTB+ step in a SEAMM flowchart """ +import configparser import copy - import logging from pathlib import Path import pprint import shutil -import subprocess import traceback import pandas -import psutil import cms_plots import dftbplus_step from .dftbplus import deep_merge, dict_to_hsd, parse_gen_file from molsystem.elements import to_symbols import seamm +import seamm_exec import seamm_util.printing as printing # In addition to the normal logger, two logger-like printing facilities are @@ -149,22 +148,41 @@ def create_band_structure_data( wd = Path(self.directory) logger.info(f"Preparing the band structure, {wd}") + seamm_options = self.parent.global_options + spin_polarized = len(Efermi) == 2 - options = self.parent.options - exe = Path(options["dftbplus_path"]) / "dp_bands" - band_path = str(wd / "band") + # Read configuration file for DFTB+ + ini_dir = Path(seamm_options["root"]).expanduser() + full_config = configparser.ConfigParser() + full_config.read(ini_dir / "dftbplus.ini") + + executor = self.parent.flowchart.executor + executor_type = executor.name + if executor_type not in full_config: + raise RuntimeError( + f"No section for '{executor_type}' in DFTB+ ini file " + f"({ini_dir / 'dftbplus.ini'})" + ) + config = dict(full_config.items(executor_type)) + if spin_polarized: - command = f"'{exe}' -s '{input_path}' '{band_path}'" + cmd = ["dp_bands", "-s", input_path, "band"] else: - command = f"'{exe}' '{input_path}' '{band_path}'" - try: - subprocess.check_output( - command, shell=True, text=True, stderr=subprocess.STDOUT - ) - except subprocess.CalledProcessError as e: - logger.warning(f"Calling dp_band, returncode = {e.returncode}") - logger.warning(f"Output: {e.output}") + cmd = ["dp_bands", input_path, "band"] + + result = executor.run( + cmd=cmd, + config=config, + directory=self.directory, + files={}, + return_files=["*"], + in_situ=True, + shell=True, + ) + + if result is None: + logger.error("There was an error running the DOS code") return None if spin_polarized: @@ -253,23 +271,47 @@ def create_dos_data(self, input_path, Efermi=[0.0]): logger.info("Preparing DOS") - options = self.parent.options + seamm_options = self.parent.global_options # Total DOS - wd = Path(self.directory) - exe = Path(options["dftbplus_path"]) / "dp_dos" - dat_file = str(wd / "dos_total.dat") - command = f"'{exe}' '{input_path}' '{dat_file}'" - try: - subprocess.check_output( - command, shell=True, text=True, stderr=subprocess.STDOUT + executor = self.parent.flowchart.executor + + # Read configuration file for DFTB+ + ini_dir = Path(seamm_options["root"]).expanduser() + full_config = configparser.ConfigParser() + full_config.read(ini_dir / "dftbplus.ini") + executor_type = executor.name + if executor_type not in full_config: + raise RuntimeError( + f"No section for '{executor_type}' in DFTB+ ini file " + f"({ini_dir / 'dftbplus.ini'})" ) - except subprocess.CalledProcessError as e: - logger.warning(f"Calling dp_dos, returncode = {e.returncode}") - logger.warning(f"Output: {e.output}") + config = dict(full_config.items(executor_type)) + + result = executor.run( + cmd=[ + "dp_dos", + str(input_path), + "dos_total.dat", + ">", + "DOS.out", + "2>", + "dos_stderr.txt", + ], + config=config, + directory=self.directory, + files={}, + return_files=["*"], + in_situ=True, + shell=True, + ) + + if result is None: + logger.error("There was an error running the DOS code") return None # Read the total DOS data + wd = Path(self.directory) with open(wd / "dos_total.dat", "r") as fd: DOS = pandas.read_csv( fd, @@ -321,15 +363,28 @@ def create_dos_data(self, input_path, Efermi=[0.0]): shell = ("s", "p", "d", "f")[shell_no - 1] label = element + "_" + shell - command = f"'{exe}' -w '{path}' '{out}'" - try: - subprocess.check_output( - command, shell=True, text=True, stderr=subprocess.STDOUT - ) - except subprocess.CalledProcessError as e: - logger.warning(f"Calling dp_dos, returncode = {e.returncode}") - logger.warning(f"Output: {e.output}") - continue + result = executor.run( + cmd=[ + "dp_dos", + "-w", + str(path), + str(out), + ">", + "DOS.out", + "2>", + "dos_stderr.txt", + ], + config=config, + directory=self.directory, + files={}, + return_files=["*"], + in_situ=True, + shell=True, + ) + + if result is None: + logger.error("There was an error running the DOS code") + return None # Read the plot data with open(out, "r") as fd: @@ -654,6 +709,10 @@ def run(self, current_input): """ system, configuration = self.get_system_configuration(None) + P = self.parameters.current_values_to_dict( + context=seamm.flowchart_variables._data + ) + directory = Path(self.directory) directory.mkdir(parents=True, exist_ok=True) @@ -675,127 +734,112 @@ def run(self, current_input): for value in self.description: printer.important(value) - files = {"dftb_in.hsd": hsd} - logger.info("dftb_in.hsd:\n" + files["dftb_in.hsd"]) - - # If the charge file exists, use it! - path = directory / "charges.dat" - if path.exists(): - files["charges.dat"] = path.read_text() - - # Write the input files to the current directory - for filename in files: - path = directory / filename - with path.open(mode="w") as fd: - fd.write(files[filename]) - - # How to run: how many processors does this node have? - n_cores = psutil.cpu_count(logical=False) - self.logger.info("The number of cores is {}".format(n_cores)) - - if seamm_options["ncores"] != "available": - n_cores = min(n_cores, int(seamm_options["ncores"])) - - if options["use_openmp"]: - n_atoms = configuration.n_atoms - n_atoms_per_core = int(options["natoms_per_core"]) - n_threads = int(round(n_atoms / n_atoms_per_core)) - if n_threads > n_cores: - n_threads = n_cores - elif n_threads < 1: - n_threads = 1 - else: - n_threads = 1 - self.logger.info( - f"DFTB+ will use {n_threads} OpenMP threads for {n_atoms} atoms." - ) - printer.important( - f" DFTB+ using {n_threads} OpenMP threads for {n_atoms} atoms.\n" - ) + # Check for successful run, don't rerun + success = directory / "success.dat" + if not success.exists(): + files = {"dftb_in.hsd": hsd} + logger.info("dftb_in.hsd:\n" + files["dftb_in.hsd"]) - env = { - "OMP_NUM_THREADS": str(n_threads), - } - - return_files = [ - "*.out", - "charges.*", - "dftb_pin.hsd", - "geom.out.*", - "output", - "pdos*", - "results.tag", - "*.xml", - "eigenvec.bin", - ] # yapf: disable - - # Run the calculation - local = seamm.ExecLocal() - exe = Path(options["dftbplus_path"]) / "dftb+" - result = local.run( - cmd=[str(exe)], - files=files, - return_files=return_files, - env=env, - in_situ=True, - directory=str(directory), - ) + # If the charge file exists, use it! + path = directory / "charges.dat" + if path.exists(): + files["charges.dat"] = path.read_text() - if result is None: - logger.error("There was an error running DFTB+") - return None + # Write the input files to the current directory + for filename in files: + path = directory / filename + with path.open(mode="w") as fd: + fd.write(files[filename]) + + if not P["input only"]: + # Get the computational environment and set limits + ce = seamm_exec.computational_environment() + + n_cores = ce["NTASKS"] + if seamm_options["ncores"] != "available": + n_cores = min(n_cores, int(seamm_options["ncores"])) + + if options["use_openmp"]: + n_atoms = configuration.n_atoms + n_atoms_per_core = int(options["natoms_per_core"]) + n_threads = int(round(n_atoms / n_atoms_per_core)) + if n_threads > n_cores: + n_threads = n_cores + elif n_threads < 1: + n_threads = 1 + else: + n_threads = 1 + printer.important( + f" DFTB+ using {n_threads} OpenMP threads for {n_atoms} " + "atoms.\n" + ) - logger.debug("\n" + pprint.pformat(result)) + env = { + "OMP_NUM_THREADS": str(n_threads), + } - logger.info("stdout:\n" + result["stdout"]) - if result["stderr"] != "": - logger.warning("stderr:\n" + result["stderr"]) + return_files = [ + "*.out", + "charges.*", + "dftb_pin.hsd", + "geom.out.*", + "output", + "pdos*", + "results.tag", + "*.xml", + "eigenvec.bin", + ] # yapf: disable + + # Run the calculation + executor = self.parent.flowchart.executor + + # Read configuration file for DFTB+ + ini_dir = Path(seamm_options["root"]).expanduser() + full_config = configparser.ConfigParser() + full_config.read(ini_dir / "dftbplus.ini") + executor_type = executor.name + if executor_type not in full_config: + raise RuntimeError( + f"No section for '{executor_type}' in DFTB+ ini file " + f"({ini_dir / 'dftbplus.ini'})" + ) + config = dict(full_config.items(executor_type)) + + result = executor.run( + cmd=["{code}", ">", "DFTB+.out", "2>", "stderr.txt"], + config=config, + directory=self.directory, + files=files, + return_files=return_files, + in_situ=True, + shell=True, + env=env, + ) - # Write the output files to the current directory - if "stdout" in result and result["stdout"] != "": - path = directory / "stdout.txt" - with path.open(mode="w") as fd: - fd.write(result["stdout"]) + if result is None: + logger.error("There was an error running DFTB+") + return None - if result["stderr"] != "": - self.logger.warning("stderr:\n" + result["stderr"]) - path = directory / "stderr.txt" - with path.open(mode="w") as fd: - fd.write(result["stderr"]) + logger.debug("\n" + pprint.pformat(result)) - for filename in result["files"]: - if filename[0] == "@": - subdir, fname = filename[1:].split("+") - path = directory / subdir / fname + if not P["input only"]: + # Parse the results.tag file + path = directory / "results.tag" + if path.exists(): + data = path.read_text() + self.results = self.parse_results(data) else: - path = directory / filename - if result[filename]["data"] is not None: - if isinstance(result[filename]["data"], bytes): - with path.open(mode="wb") as fd: - fd.write(result[filename]["data"]) - else: - with path.open(mode="w") as fd: - fd.write(result[filename]["data"]) - else: - with path.open(mode="w") as fd: - fd.write(result[filename]["exception"]) + self.results = {} - # Parse the results.tag file - if "results.tag" in result["files"]: - self.results = self.parse_results(result["results.tag"]["data"]) - else: - self.results = {} - - # And a final structure - if "geom.out.gen" in result["files"]: - self.results["final structure"] = parse_gen_file( - result["geom.out.gen"]["data"] - ) + # And a final structure + path = directory / "geom.out.gen" + if path.exists(): + data = path.read_text() + self.results["final structure"] = parse_gen_file(data) - # Analyze the results - self.analyze(data=self.results) - printer.important(" ") + # Analyze the results + self.analyze(data=self.results) + printer.important(" ") - # Add other citations here or in the appropriate place in the code. - # Add the bibtex to data/references.bib, and add a self.reference.cite - # similar to the above to actually add the citation to the references. + # Ran successfully, put out the success file + success.write_text("success") diff --git a/dftbplus_step/energy.py b/dftbplus_step/energy.py index 813d798..09cad5f 100644 --- a/dftbplus_step/energy.py +++ b/dftbplus_step/energy.py @@ -2,6 +2,7 @@ """Setup DFTB+""" +import configparser import csv import gzip @@ -13,7 +14,6 @@ import logging from pathlib import Path import shutil -import subprocess import textwrap import hsd @@ -204,6 +204,42 @@ def get_input(self): parameter_set = self.model model, parameter_set_name = parameter_set.split("/", 1) + # Main control of the Hamiltonian + if model == "DFTB": + # template + result = { + "Analysis": { + "CalculateForces": "Yes", + }, + "Hamiltonian": {"DFTB": {}}, + } + hamiltonian = result["Hamiltonian"]["DFTB"] + else: + result = { + "Analysis": { + "CalculateForces": "Yes", + }, + "Hamiltonian": {"xTB": {}}, + } + hamiltonian = result["Hamiltonian"]["xTB"] + + # Basic parameters + hamiltonian["SCC"] = P["SCC"] + if P["SCC"] == "Yes": + hamiltonian["SCCTolerance"] = P["SCCTolerance"] + hamiltonian["MaxSCCIterations"] = P["MaxSCCIterations"] + hamiltonian["ConvergentSccOnly"] = "No" + if P["Filling"] == "Gaussian": + filling = hamiltonian["Filling = MethfesselPaxton"] = {} + filling["Order"] = 1 + if P["Filling"] == "Methfessel-Paxton": + filling = hamiltonian["Filling = MethfesselPaxton"] = {} + filling["Order"] = 2 + else: + filling = hamiltonian["Filling = Fermi"] = {} + filling["Temperature [K]"] = P["Filling Temperature"].m_as("K") + + # Detail for the models if model == "DFTB": if "defaults" in dataset: all_defaults = {**dataset["defaults"]} @@ -219,64 +255,9 @@ def get_input(self): else: defaults = {} - # template - result = { - "Analysis": { - "CalculateForces": "Yes", - }, - "Hamiltonian": {"DFTB": {}}, - } - hamiltonian = result["Hamiltonian"]["DFTB"] - if P["SCC"] == "Yes": - hamiltonian["SCC"] = "Yes" - hamiltonian["SCCTolerance"] = P["SCCTolerance"] - hamiltonian["MaxSCCIterations"] = P["MaxSCCIterations"] - hamiltonian["ShellResolvedSCC"] = P["ShellResolvedSCC"].capitalize() - - have_charges = "charge" in atoms - if have_charges: - for tmp in atoms["charge"]: - if tmp is None: - have_charges = False - break - initial_charges = P["initial charges"] - if initial_charges == "default": - # Use charge file from previous step or charges on atoms - if self.get_previous_charges(missing_ok=True) is not None: - hamiltonian["ReadInitialCharges"] = "Yes" - elif have_charges: - if periodicity == 0: - charges = [*atoms["charge"]] - else: - charges = [ - atoms["charge"][i] for i in self.mapping_from_primitive - ] - # Ensure sums exactly to charge - delta = (configuration.charge - sum(charges)) / len(charges) - hamiltonian["InitialCharges"] = { - "AllAtomCharges": "{" - + f"{', '.join(str(c + delta) for c in charges)}" - + "}" - } - elif initial_charges == "from previous step": - if self.get_previous_charges(): - hamiltonian["ReadInitialCharges"] = "Yes" - elif initial_charges == "from structure": - if have_charges: - if periodicity == 0: - charges = [*atoms["charge"]] - else: - charges = [ - atoms["charge"][i] for i in self.mapping_from_primitive - ] - # Ensure sums exactly to charge - delta = (configuration.charge - sum(charges)) / len(charges) - hamiltonian["InitialCharges"] = { - "AllAtomCharges": "{" - + f"{', '.join(str(c + delta) for c in charges)}" - + "}" - } + # mixer = hamiltonian["Mixing = Simple"] = {} + # mixer["MixingParameter"] = 0.05 third_order = P["ThirdOrder"] if third_order == "Default for parameters": @@ -324,17 +305,56 @@ def get_input(self): else: damping = P["Damping Exponent"] hamiltonian["Damping Exponent"] = damping - elif model == "xTB": - result = { - "Analysis": { - "CalculateForces": "Yes", - }, - "Hamiltonian": {"xTB": {}}, - } - hamiltonian = result["Hamiltonian"]["xTB"] - hamiltonian["SCC"] = "Yes" - hamiltonian["SCCTolerance"] = P["SCCTolerance"] - hamiltonian["MaxSCCIterations"] = P["MaxSCCIterations"] + + have_charges = False + if "charge" in atoms: + have_charges = True + charges = "charge" + elif "formal_charge" in atoms: + have_charges = True + charges = "formal_charge" + if have_charges: + for tmp in atoms[charges]: + if tmp is None: + have_charges = False + break + initial_charges = P["initial charges"] + if initial_charges == "default": + # Use charge file from previous step or charges on atoms + if self.get_previous_charges(missing_ok=True) is not None: + hamiltonian["ReadInitialCharges"] = "Yes" + elif have_charges: + if periodicity == 0: + charges = [*atoms[charges]] + else: + charges = [ + atoms[charges][i] for i in self.mapping_from_primitive + ] + # Ensure sums exactly to charge + delta = (configuration.charge - sum(charges)) / len(charges) + hamiltonian["InitialCharges"] = { + "AllAtomCharges": "{" + + f"{', '.join(str(c + delta) for c in charges)}" + + "}" + } + elif initial_charges == "from previous step": + if self.get_previous_charges(): + hamiltonian["ReadInitialCharges"] = "Yes" + elif initial_charges == "from structure": + if have_charges: + if periodicity == 0: + charges = [*atoms[charges]] + else: + charges = [ + atoms[charges][i] for i in self.mapping_from_primitive + ] + # Ensure sums exactly to charge + delta = (configuration.charge - sum(charges)) / len(charges) + hamiltonian["InitialCharges"] = { + "AllAtomCharges": "{" + + f"{', '.join(str(c + delta) for c in charges)}" + + "}" + } # Handle charge and spin hamiltonian["Charge"] = configuration.charge @@ -347,122 +367,126 @@ def get_input(self): have_spins = False break - if P["SpinPolarisation"] == "none": - hamiltonian["SpinPolarisation"] = {} - elif ( - periodicity == 0 - and multiplicity == 1 - and P["SpinPolarisation"] == "from system" - ): - hamiltonian["SpinPolarisation"] = {} - elif ( - periodicity != 0 - and P["SpinPolarisation"] == "from system" - and multiplicity == 1 - and not have_spins - ): - hamiltonian["SpinPolarisation"] = {} - else: - noncollinear = P["SpinPolarisation"] == "noncollinear" - - H = hamiltonian["SpinPolarisation"] = {} - if noncollinear: - section = H["NonCollinear"] = {} + if P["SCC"] == "Yes": + if P["SpinPolarisation"] == "none": + hamiltonian["SpinPolarisation"] = {} + elif ( + periodicity == 0 + and multiplicity == 1 + and P["SpinPolarisation"] == "from system" + ): + hamiltonian["SpinPolarisation"] = {} + elif ( + periodicity != 0 + and P["SpinPolarisation"] == "from system" + and multiplicity == 1 + and not have_spins + ): + hamiltonian["SpinPolarisation"] = {} else: - section = H["Collinear"] = {} - reading_charge_file = ( - "ReadInitialCharges" in hamiltonian - and hamiltonian["ReadInitialCharges"] == "Yes" - ) - if not reading_charge_file: - if have_spins: - if periodicity == 0: - spins = atoms["spin"] + noncollinear = P["SpinPolarisation"] == "noncollinear" + + H = hamiltonian["SpinPolarisation"] = {} + if noncollinear: + section = H["NonCollinear"] = {} + else: + section = H["Collinear"] = {} + reading_charge_file = ( + "ReadInitialCharges" in hamiltonian + and hamiltonian["ReadInitialCharges"] == "Yes" + ) + if not reading_charge_file: + if have_spins: + if periodicity == 0: + spins = atoms["spin"] + else: + spins = [ + atoms["spin"][i] + for i in self.mapping_from_primitive + ] + section["InitialSpins"] = { + "AllAtomSpins": "{" + + f"{', '.join(str(c) for c in spins)}" + + "}" + } else: - spins = [ - atoms["spin"][i] for i in self.mapping_from_primitive - ] - section["InitialSpins"] = { - "AllAtomSpins": "{" - + f"{', '.join(str(c) for c in spins)}" - + "}" - } - else: - section["UnpairedElectrons"] = multiplicity - 1 - - section["RelaxTotalSpin"] = P["RelaxTotalSpin"].capitalize() - - # Get the spin constants - package = self.__module__.split(".")[0] - files = [ - p for p in implib.files(package) if p.name == "spin-constants.json" - ] - if len(files) != 1: - raise RuntimeError( - "Can't find spin-constants.json file. Check the installation!" - ) - data = files[0].read_text() - spin_constant_data = json.loads(data) - - # First check if we have shell resolved constants or not - spin_constants = hamiltonian["SpinConstants"] = {} - symbols = sorted([*set(atoms.symbols)]) - dataset_name = self.model - # e.g. "DFTB - mio" - key = dataset_name.split("/")[1] - if key in spin_constant_data: - constants = spin_constant_data[key] - else: - constants = spin_constant_data["GGA"] - - # Bit of a kludgy test. If not shell-resolved there is one constant - # per shell, i.e. 1, 2 or 3 for s, p, d. If resolved, there are 1, 4, 9. - shell_resolved = False - for symbol in symbols: - if len(constants[symbol]) > 3: - shell_resolved = True - break + section["UnpairedElectrons"] = multiplicity - 1 + + section["RelaxTotalSpin"] = P["RelaxTotalSpin"].capitalize() + + # Get the spin constants + package = self.__module__.split(".")[0] + files = [ + p for p in implib.files(package) if p.name == "spin-constants.json" + ] + if len(files) != 1: + raise RuntimeError( + "Can't find spin-constants.json file. Check the installation!" + ) + data = files[0].read_text() + spin_constant_data = json.loads(data) + + # First check if we have shell resolved constants or not + spin_constants = hamiltonian["SpinConstants"] = {} + symbols = sorted([*set(atoms.symbols)]) + dataset_name = self.model + # e.g. "DFTB - mio" + key = dataset_name.split("/")[1] + if key in spin_constant_data: + constants = spin_constant_data[key] + else: + constants = spin_constant_data["GGA"] - if shell_resolved: - if P["ShellResolvedSpin"] == "yes": - spin_constants["ShellResolvedSpin"] = "Yes" + # Bit of a kludgy test. If not shell-resolved there is one constant + # per shell, i.e. 1, 2 or 3 for s, p, d. If resolved, there are 1, 4, 9. + shell_resolved = False + for symbol in symbols: + if len(constants[symbol]) > 3: + shell_resolved = True + break + + if shell_resolved: + if P["ShellResolvedSpin"] == "yes": + spin_constants["ShellResolvedSpin"] = "Yes" + else: + spin_constants["ShellResolvedSpin"] = "No" + shell_resolved = False else: spin_constants["ShellResolvedSpin"] = "No" - shell_resolved = False - else: - spin_constants["ShellResolvedSpin"] = "No" - # And add them and the control parameters - if shell_resolved: - for symbol in symbols: - spin_constants[symbol] = ( - "{" + " ".join([str(c) for c in constants[symbol]]) + "}" - ) - else: - for symbol in symbols: - shells = element_data[symbol]["electron configuration"] - shell = shells.split()[-1] - tmp = constants[symbol] - if "s" in shell: - spin_constants[symbol] = str(tmp[0]) - elif "p" in shell: - if len(tmp) == 4: - spin_constants[symbol] = str(tmp[3]) - elif len(tmp) == 9: - spin_constants[symbol] = str(tmp[4]) - else: - raise RuntimeError( - f"Error in spin constants for {symbol}: {tmp}" - ) - elif "d" in shell: - if len(tmp) == 9: - spin_constants[symbol] = str(tmp[8]) + # And add them and the control parameters + if shell_resolved: + for symbol in symbols: + spin_constants[symbol] = ( + "{" + " ".join([str(c) for c in constants[symbol]]) + "}" + ) + else: + for symbol in symbols: + shells = element_data[symbol]["electron configuration"] + shell = shells.split()[-1] + tmp = constants[symbol] + if "s" in shell: + spin_constants[symbol] = str(tmp[0]) + elif "p" in shell: + if len(tmp) == 4: + spin_constants[symbol] = str(tmp[3]) + elif len(tmp) == 9: + spin_constants[symbol] = str(tmp[4]) + else: + raise RuntimeError( + f"Error in spin constants for {symbol}: {tmp}" + ) + elif "d" in shell: + if len(tmp) == 9: + spin_constants[symbol] = str(tmp[8]) + else: + raise RuntimeError( + f"Error in spin constants for {symbol}: {tmp}" + ) else: raise RuntimeError( - f"Error in spin constants for {symbol}: {tmp}" + f"Can't handle spin constants for {symbol}" ) - else: - raise RuntimeError(f"Can't handle spin constants for {symbol}") # Integration grid in reciprocal space if configuration.periodicity == 3: @@ -959,19 +983,34 @@ def make_plots(self, data): hsd.dump(input_data, str(path)) # And run WAVEPLOT - cmd = str(Path(self.parent.options["dftbplus_path"]) / "waveplot") - try: - output = subprocess.check_output( - cmd, shell=True, text=True, stderr=subprocess.STDOUT, cwd=directory - ) - except subprocess.CalledProcessError as e: - return ( - f"Calling waveplot, returncode = {e.returncode}" - f"\n\nOutput: {e.output}" + seamm_options = self.parent.global_options + # Read configuration file for DFTB+ + ini_dir = Path(seamm_options["root"]).expanduser() + full_config = configparser.ConfigParser() + full_config.read(ini_dir / "dftbplus.ini") + + executor = self.parent.flowchart.executor + executor_type = executor.name + if executor_type not in full_config: + raise RuntimeError( + f"No section for '{executor_type}' in DFTB+ ini file " + f"({ini_dir / 'dftbplus.ini'})" ) + config = dict(full_config.items(executor_type)) + + result = executor.run( + cmd=["waveplot", ">", "waveplot.out"], + config=config, + directory=self.directory, + files={}, + return_files=["*"], + in_situ=True, + shell=True, + ) - path = directory / "waveplot.out" - path.write_text(output) + if result is None: + logger.error("There was an error running the DOS code") + return None # Finally rename and gzip the cube files n_processed = 0 diff --git a/dftbplus_step/energy_parameters.py b/dftbplus_step/energy_parameters.py index 1666cc5..f2b9cc9 100644 --- a/dftbplus_step/energy_parameters.py +++ b/dftbplus_step/energy_parameters.py @@ -62,6 +62,18 @@ class EnergyParameters(seamm.Parameters): """ parameters = { + "input only": { + "default": "no", + "kind": "boolean", + "default_units": "", + "enumeration": ( + "yes", + "no", + ), + "format_string": "s", + "description": "Write the input files and stop:", + "help_text": "Don't run DFTB+. Just write the input files.", + }, "primitive cell": { "default": "Yes", "kind": "boolean", @@ -105,6 +117,33 @@ class EnergyParameters(seamm.Parameters): "steps, the program stops unless requested elsewhere." ), }, + "ConvergentSccOnly": { + "default": "No", + "kind": "string", + "default_units": "", + "enumeration": ("Yes", "No"), + "format_string": "", + "description": "Not converged is error:", + "help_text": "Whether to throw an error if the SCC cycle doesn't converge.", + }, + "Filling": { + "default": "Fermi", + "kind": "string", + "default_units": "", + "enumeration": ("Fermi", "Gaussian", "Methfessel-Paxton"), + "format_string": "", + "description": "Electronic temperature method:", + "help_text": "The method to populate the electrons due to temperature.", + }, + "Filling Temperature": { + "default": 300, + "kind": "float", + "default_units": "K", + "enumeration": None, + "format_string": "", + "description": "Temperature:", + "help_text": "The electronic temperature for the electronic population.", + }, "ShellResolvedSCC": { "default": "no", "kind": "string", diff --git a/dftbplus_step/metadata.py b/dftbplus_step/metadata.py index 5df552f..ea478b9 100644 --- a/dftbplus_step/metadata.py +++ b/dftbplus_step/metadata.py @@ -99,6 +99,15 @@ "dimensionality": [3, "nspins"], "type": "float", }, + "scaled_dipole": { + "calculation": [ + "energy", + "optimization", + ], + "description": "The scaled dipole moments of the system", + "dimensionality": [3, "nspins"], + "type": "float", + }, "eigenvalues": { "calculation": [ "energy", diff --git a/dftbplus_step/optimization_parameters.py b/dftbplus_step/optimization_parameters.py index 7daa280..ed45a48 100644 --- a/dftbplus_step/optimization_parameters.py +++ b/dftbplus_step/optimization_parameters.py @@ -21,6 +21,7 @@ class OptimizationParameters(dftbplus_step.EnergyParameters): "Rational Function", "Limited-memory Broyden-Fletcher-Goldfarb-Shanno (LBFGS)", "Fast inertial relaxation engine (FIRE)", + "Steepest descent", ), "format_string": "s", "description": "Method:", diff --git a/dftbplus_step/tk_energy.py b/dftbplus_step/tk_energy.py index 58d9e86..bcc0d5b 100644 --- a/dftbplus_step/tk_energy.py +++ b/dftbplus_step/tk_energy.py @@ -58,6 +58,9 @@ def create_dialog(self, title="Edit DFTB+ Energy Step"): # Create all the widgets P = self.node.parameters + # Just write input + self["input only"] = P["input only"].widget(self["frame"]) + # Frame to isolate widgets e_frame = self["energy frame"] = ttk.LabelFrame( self["frame"], @@ -69,7 +72,7 @@ def create_dialog(self, title="Edit DFTB+ Energy Step"): ) for key in dftbplus_step.EnergyParameters.parameters: - if key not in ("results", "extra keywords", "create tables"): + if key not in ("results", "extra keywords", "create tables", "input only"): self[key] = P[key].widget(e_frame) # Set the callbacks for changes @@ -117,8 +120,12 @@ def reset_dialog(self, widget=None): for slave in frame.grid_slaves(): slave.grid_forget() - # Put in the energy frame row = 0 + # Whether to just write input + self["input only"].grid(row=row, column=0, sticky=tk.W) + row += 1 + + # Put in the energy frame self["energy frame"].grid(row=row, column=0, sticky=tk.N) row += 1 @@ -149,21 +156,30 @@ def reset_energy_frame(self, widget=None): widgets.append(self["SCC"]) row += 1 if scc: - for widget in ("SCCTolerance", "MaxSCCIterations", "ThirdOrder"): + for widget in ("SCCTolerance", "MaxSCCIterations", "ConvergentSccOnly"): self[widget].grid(row=row, column=1, columnspan=2, sticky=tk.EW) widgets1.append(self[widget]) row += 1 - self["HCorrection"].grid(row=row, column=1, columnspan=2, sticky=tk.EW) - widgets1.append(self["HCorrection"]) - row += 1 + for widget in ("ThirdOrder", "HCorrection"): + self[widget].grid(row=row, column=1, columnspan=2, sticky=tk.EW) + widgets1.append(self[widget]) + row += 1 hcorrection = self["HCorrection"].get() if hcorrection == "Damping": - self["Damping Exponent"].grid(row=row, column=1, sticky=tk.EW) + self["Damping Exponent"].grid(row=row, column=2, sticky=tk.EW) widgets2.append(self["Damping Exponent"]) row += 1 + for widget in ("Filling",): + self[widget].grid(row=row, column=1, columnspan=2, sticky=tk.EW) + widgets1.append(self[widget]) + row += 1 + self["Filling Temperature"].grid(row=row, column=2, sticky=tk.EW) + widgets2.append(self["Filling Temperature"]) + row += 1 + # The Brillouin zone integration grid kmethod = self["k-grid method"].get() self["k-grid method"].grid(row=row, column=0, columnspan=3, sticky=tk.EW) @@ -188,7 +204,8 @@ def reset_energy_frame(self, widget=None): sw.align_labels(widgets1, sticky=tk.E) sw.align_labels(widgets2, sticky=tk.E) - frame.columnconfigure(0, minsize=30) + frame.columnconfigure(0, minsize=40) + frame.columnconfigure(1, minsize=200) return row diff --git a/dftbplus_step/tk_optimization.py b/dftbplus_step/tk_optimization.py index b25c2ff..8b2bf23 100644 --- a/dftbplus_step/tk_optimization.py +++ b/dftbplus_step/tk_optimization.py @@ -94,7 +94,7 @@ def create_dialog(self, title="Edit DFTB+ Optimization Step"): def reset_dialog(self, widget=None): super().reset_dialog() - row = 0 + row = 1 self["optimization frame"].grid(row=row, column=1, sticky=tk.EW) row += 1 self["structure frame"].grid(row=row, column=0, columnspan=2) diff --git a/requirements.txt b/requirements.txt index 31a8c2b..be52b7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ cms_plots hsd pandas -psutil seamm +seamm-exec seamm_util tabulate