def readE(itt): nonlocal name_conv # read the energy tables f = self.step_to("Free energy of the ion-electron system", reread=False)[0] if not f: return None next(itt) # ----- line = next(itt) E = PropertyDict() while "----" not in line: key, *v = line.split() if key == "PAW": E[f"{name_conv[key]}1"] = float(v[-2]) E[f"{name_conv[key]}2"] = float(v[-1]) else: E[name_conv[key]] = float(v[-1]) line = next(itt) line = next(itt) E["free"] = float(line.split()[-2]) line = next(itt) line = next(itt) v = line.split() E["total"] = float(v[4]) E["sigma0"] = float(v[-1]) return E
def reset(self, out=None, norm=None): """ Reset data table to be able to restart """ # While this *could* be a named-tuple, we would not be able # to override the attribute, hence we use a property dict # same effect. self.data = PropertyDict(x=[], y=[], hash=[]) # log log = "" if not out is None: log += f" out={str(out)}" self.out = Path(out) if not norm is None: log += f" norm={str(norm)}" if isinstance(norm, str): self.norm = (norm, 1.) elif isinstance(norm, Real): self.norm = ("l2", norm) else: self.norm = norm _log.info(f"{self.__class__.__name__} resetting{log}")
class AtomInput: """Input for the ``atom`` program see `[1]`_ This class enables the construction of the ``INP`` file to be fed to ``atom``. # Example input for ATOM # # Comments allowed here # # ae Si ground state all-electron # Si car # 0.0 # 3 2 # 3 0 2.00 0.00 # 3 1 2.00 0.00 # # Comments allowed here # #2345678901234567890123456789012345678901234567890 Ruler .. [1]: https://siesta.icmab.es/SIESTA_MATERIAL/Pseudos/atom_licence.html """ def __init__(self, atom, define=('NEW_CC', 'FREE_FORMAT_RC_INPUT', 'NO_PS_CUTOFFS'), **opts): # opts = { # "flavor": "tm2", # "xc": "pb", # optionally libxc # "equation": "r", # "logr": 2. # "cc": False, # "rcore": 2. # } self.atom = atom assert isinstance(atom, si.Atom) if "." in self.atom.tag: raise ValueError("The atom 'tag' must not contain a '.'!") # We need to check that atom has 4 orbitals, with increasing l # We don't care about n or any other stuff, so these could be # SphericalOrbital, for that matter l = 0 for orb in self.atom: if orb.l != l: raise ValueError( f"{self.__class__.__name__} atom argument does not have " f"increasing l quantum number index {l} has l={orb.l}") l += 1 if l != 4: raise ValueError( f"{self.__class__.__name__} atom argument must have 4 orbitals. " f"One for each s-p-d-f shell") self.opts = PropertyDict(**opts) # Check options passed and define defaults self.opts.setdefault("equation", "r") if self.opts.equation not in ' rs': # ' ' == non-polarized # s == polarized # r == relativistic raise ValueError( f"{self.__class__.__name__} failed to initialize; opts{'equation': <v>} has wrong value, should be [ rs]." ) if self.opts.equation == 's': raise NotImplementedError( f"{self.__class__.__name__} does not implement spin-polarized option (use relativistic)" ) self.opts.setdefault("flavor", "tm2") if self.opts.flavor not in ('hsc', 'ker', 'tm2'): # hsc == Hamann-Schluter-Chiang # ker == Kerker # tm2 == Troullier-Martins raise ValueError( f"{self.__class__.__name__} failed to initialize; opts{'flavor': <v>} has wrong value, should be [hsc|ker|tm2]." ) self.opts.setdefault("logr", 2.) # default to true if set self.opts.setdefault("cc", "rcore" in self.opts) # rcore only used if cc is True self.opts.setdefault("rcore", 2.) self.opts.setdefault("xc", "pb") # Read in the core valence shells for this atom # figure out what the default value is. # We do this my finding the minimum index of valence shells # in the _shell_order list, then we use that as the default number # of core-shells occpupied # e.g if the minimum valence shell is 2p, it would mean that # _shell_order.index("2p") == 2 # which has 1s and 2s occupied. spdf = 'spdf' try: core = reduce(min, (_shell_order.index(f"{orb.n}{spdf[orb.l]}") for orb in atom), len(_shell_order)) except: core = -1 self.opts.setdefault("core", core) if self.opts.core == -1: raise ValueError( f"Default value for {self.atom.symbol} not added, please add core= at instantiation" ) # Store the defined names if define is None: self.define = [] elif isinstance(define, str): self.define = [define] else: # must be list-like self.define = define @classmethod def from_input(cls, inp): """ Return atom object respecting the input Parameters ---------- inp : list or str create `AtomInput` from the content of `inp` """ def _get_content(f): if f.is_file(): return open(f, 'r').readlines() return None if isinstance(inp, (tuple, list)): # it is already in correct format pass elif isinstance(inp, (str, Path)): # convert to path inp = Path(inp) # Check if it is a path or an input content = _get_content(inp) if content is None: content = _get_content(inp / "INP") if content is None: raise ValueError( f"Could not find any input file in {str(inp)} or {str(inp / 'INP')}" ) inp = content else: raise ValueError(f"Unknown input format inp={inp}?") # Now read lines defines = [] opts = PropertyDict() def bypass_comments(inp): if inp[0].startswith("#"): inp.pop(0) bypass_comments(inp) def bypass(inp, defines): bypass_comments(inp) if inp[0].startswith("%define"): line = inp.pop(0) defines.append(line.split()[1].strip()) bypass(inp, defines) bypass(inp, defines) # Now prepare reading # First line has to contain the *type* of calculation # pg|pe|ae|pt <comment> line = inp.pop(0).strip() if line.startswith("pg"): opts.cc = False elif line.startswith("pe"): opts.cc = True # <flavor> logr? line = inp.pop(0).strip().split() opts.flavor = line[0] if len(line) >= 2: opts.logr = float(line[1]) / _Ang2Bohr # <element> <xc>' rs'? line = inp.pop(0) symbol = line.split()[0] # now get xc equation if len(line) >= 11: opts.equation = line[10:10] opts.xc = line[:10].split()[1] line = line.split() if len(line) >= 3: opts.libxc = int(line[2]) # currently not used line inp.pop(0) # core, valence core, valence = inp.pop(0).split() opts.core = int(core) valence = int(valence) orbs = [] for _ in range(valence): n, l, *occ = inp.pop(0).split() orb = PropertyDict() orb.n = int(n) orb.l = int(l) # currently we don't distinguish between up/down orb.q0 = sum(map(float, occ)) orbs.append(orb) # now we read the line with rc's and core-correction rcs = inp.pop(0).split() if len(rcs) >= 6: # core-correction opts.rcore = float(rcs[5]) / _Ang2Bohr for orb in orbs: orb.R = float(rcs[orb.l]) / _Ang2Bohr # Now create orbitals orbs = [si.AtomicOrbital(**orb, m=0, zeta=1) for orb in orbs] # now re-arrange ensuring we have correct order of l shells orbs = sorted(orbs, key=lambda orb: orb.l) atom = si.Atom(symbol, orbs) return cls(atom, defines, **opts) def _write_header(self, f): f.write("# This file is generated by sisl pseudo\n") # Define all names for define in self.define: f.write(f"%define {define.upper()}\n") def _write_middle(self, f): xc = self.opts.xc equation = self.opts.equation rcore = self.opts.rcore * _Ang2Bohr f.write(f" {self.atom.symbol:2s} {xc:2s}{equation:1s}") if "libxc" in self.opts: f.write(f" {self.opts.libxc:8d}") f.write(f"\n {0.0:5.1f}\n") # now extract the charges for each orbital atom = self.atom core = self.opts.core valence = len(atom) f.write(f"{core:5d}{valence:5d}\n") orbs = sorted(atom.orbitals, key=lambda x: x.l) Rs = [0.] * 4 # always 4: s, p, d, f for orb in orbs: # Write the configuration of this orbital n = orb.n l = orb.l # for now this is a single integer q0 = orb.q0 f.write(f"{n:5d}{l:5d}{q0:10.3f}{0.0:10.3f}\n") Rs[l] = orb.R * _Ang2Bohr f.write( f"{Rs[0]:10.7f} {Rs[1]:10.7f} {Rs[2]:10.7f} {Rs[3]:10.7f} {0.0:10.7f} {rcore:10.7f}\n" ) def _get_out(self, path, filename): if path is None: return Path(filename) return Path(path) / Path(filename) def ae(self, filename="INP", path=None): out = self._get_out(path, filename) with open(out, 'w') as f: self._write_header(f) # Now prepare data f.write(f" ae {self.atom.symbol} ground state calculation\n") self._write_middle(f) def pg(self, filename="INP", path=None): # check whether we need core corrections out = self._get_out(path, filename) if self.opts.cc: # use core corrections pg = "pe" else: # do not use core corrections pg = "pg" logr = self.opts.logr * _Ang2Bohr with open(out, 'w') as f: self._write_header(f) # Now prepare data f.write( f" {pg:2s} {self.atom.symbol} pseudo potential generation\n") if logr < 0.: f.write(f" {self.opts.flavor:3s}\n") else: f.write(f" {self.opts.flavor:3s}{logr:9.3f}\n") self._write_middle(f) def plot(self, path=None, plot=('wavefunction', 'charge', 'log', 'potential'), l='spdf', show=True): """ Plot everything related to this psf file Parameters ---------- path : str or pathlib.Path, optional from which directory should files be read plot : list-like of str, optional which data to plot l : list-like, optional which l-shells to plot (for those that have l-shell decompositions) show : bool, optional call `matplotlib.pyplot.show()` at the end Returns ------- fig : figure for axes axs : axes used for plotting """ import matplotlib.pyplot as plt if path is None: path = Path.cwd() else: path = Path(path) def get_xy(f, yfactors=None): """ Return x, y data from file `f` with y being calculated as the factors between the columns """ nonlocal path f = path / f if not f.is_file(): print(f"Could not find file: {str(f)}") return None, None data = np.loadtxt(f) ncol = data.shape[1] if yfactors is None: yfactors = [0, 1] yfactors = np.pad(yfactors, (0, ncol - len(yfactors)), constant_values=0.) x = data[:, 0] y = (data * yfactors.reshape(1, -1)).sum(1) return x, y spdfg = 'spdfg' l2i = { 's': 0, 0: 0, 'p': 1, 1: 1, 'd': 2, 2: 2, 'f': 3, 3: 3, 'g': 4, 4: 4, # never used } # Get this atoms default calculated binding length # We use this one since there are many missing elements # in vdw table. # And convert to Bohr atom_r = self.atom.radius("calc") * _Ang2Bohr def plot_wavefunction(ax): # somewhat similar to ae.gplot ax.set_title("Wavefunction") ax.set_xlabel("Radius [Bohr]") for shell in l: il = l2i[shell] orb = self.atom.orbitals[il] r, w = get_xy(f"AEWFNR{il}") if not r is None: p = ax.plot(r, w, label=f"AE {spdfg[il]}") color = p[0].get_color() ax.axvline(orb.R * _Ang2Bohr, color=color, alpha=0.5) r, w = get_xy(f"PSWFNR{il}") if not r is None: ax.plot(r, w, '--', label=f"PS {spdfg[il]}") ax.set_xlim(0, atom_r * 5) ax.autoscale(enable=True, axis='y', tight=True) ax.legend() def plot_charge(ax): ax.set_title("Charge") ax.set_xlabel("Radius [Bohr]") ax.set_ylabel("(4.pi.r^2) Charge [electrons/Bohr]") # Get current core-correction length ae_r, ae_cc = get_xy("AECHARGE", [0, 0, 0, 1]) _, ae_vc = get_xy("AECHARGE", [0, 1, 1, -1]) if not ae_cc is None: p = ax.plot(ae_r, ae_cc, label=f"AE core") color = p[0].get_color() if self.opts.get("cc", False): ax.axvline(self.opts.rcore * _Ang2Bohr, color=color, alpha=0.5) ax.plot(ae_r, ae_vc, '--', label=f"AE valence") ps_r, ps_cc = get_xy("PSCHARGE", [0, 0, 0, 1]) _, ps_vc = get_xy("PSCHARGE", [0, 1, 1]) if not ps_r is None: ax.plot(ps_r, ps_cc, '--', label=f"PS core") ax.plot(ps_r, ps_vc, ':', label=f"PS valence") # Now determine the overlap between all-electron core-charge # and the pseudopotential valence charge if np.allclose(ae_r, ps_r): # Determine dR dr = ae_r[1] - ae_r[0] # Integrate number of core-electrons and valence electrons core_c = np.trapz(ae_cc, ae_r) valence_c = np.trapz(ps_vc, ps_r) print(f"Total charge in atom: {core_c + valence_c:.5f}") overlap_c = np.trapz(np.minimum(ae_cc, ps_vc), ae_r) ax.set_title( f"Charge: int(min(AE_cc, PS_vc)) = {overlap_c:.3f} e") # We will try and *guess-stimate* a good position for rc for core-corrections # Javier Junquera's document says: # r_pc has to be chosen such that the valence charge density is negligeable compared to # the core one for r < r_pc. # Tests show that it might be located where the core charge density is from 1 to 2 times # larger than the valence charge density with np.errstate(divide='ignore', invalid='ignore'): core_over_valence = ae_cc / ps_vc ax2 = ax.twinx( ) # instantiate a second axes that shares the same x-axis ax2.plot(ae_r, core_over_valence, 'k', alpha=0.5) # Now mark 1, 1.5 and 2 times factor_marks = [2., 1.5, 1] r_marks = [] for mark in factor_marks: # last value closest to function idx = (core_over_valence > mark).nonzero()[0][-1] r_marks.append(ae_r[idx]) ax2.scatter(r_marks, factor_marks, alpha=0.5) ax2.set_ylim(0, 3) print(f"Core-correction r_pc {factor_marks}: {r_marks} Bohr") ax.set_xlim(0, atom_r) ax.set_ylim(0) ax.legend() def plot_log(ax): ax.set_title("d-log of wavefunction") ax.set_xlabel("Energy [Ry]") ax.set_ylabel("Derivative of wavefunction") for shell in l: il = l2i[shell] e, log = get_xy(f"AELOGD{il}") emark = np.loadtxt(path / f"AEEV{il}") if emark.ndim == 1: emark.shape = (1, -1) emark = emark[:, 0] if not e is None: p = ax.plot(e, log, label=f"AE {spdfg[il]}") idx_mark = ( np.fabs(e.reshape(-1, 1) - emark.reshape(1, -1)).argmin(axis=0)) ax.scatter(emark, log[idx_mark], color=p[0].get_color(), alpha=0.5) # And now PS e, log = get_xy(f"PSLOGD{il}") emark = np.loadtxt(path / f"PSEV{il}") if emark.ndim == 1: emark.shape = (1, -1) emark = emark[:, 0] if not e is None: p = ax.plot(e, log, label=f"PS {spdfg[il]}") idx_mark = ( np.fabs(e.reshape(-1, 1) - emark.reshape(1, -1)).argmin(axis=0)) ax.scatter(emark, log[idx_mark], color=p[0].get_color(), alpha=0.5) ax.legend() def plot_potential(ax): ax.set_title("Pseudopotential") ax.set_xlabel("Radius [Bohr]") ax.set_ylabel("Potential [Ry]") for shell in l: il = l2i[shell] orb = self.atom.orbitals[il] r, V = get_xy(f"PSPOTR{il}") if not r is None: p = ax.plot(r, V, label=f"PS {spdfg[il]}") color = p[0].get_color() ax.axvline(orb.R * _Ang2Bohr, color=color, alpha=0.5) ax.set_xlim(0, atom_r * 3) ax.legend() nrows = len(l) // 2 ncols = len(l) // nrows if nrows * ncols < len(l): ncols += 1 fig, axs = plt.subplots(nrows, ncols, figsize=(11, 10)) def next_rc(ir, ic, nrows, ncols): ic = ic + 1 if ic == ncols: ic = 0 ir = ir + 1 return ir, ic ir, ic = 0, 0 for this_plot in map(lambda x: x.lower(), plot): if this_plot == "wavefunction": plot_wavefunction(axs[ir][ic]) elif this_plot == "log": plot_log(axs[ir][ic]) elif this_plot == "charge": plot_charge(axs[ir][ic]) elif this_plot == "potential": plot_potential(axs[ir][ic]) ir, ic = next_rc(ir, ic, nrows, ncols) if show: plt.show() return fig, axs
def from_input(cls, inp): """ Return atom object respecting the input Parameters ---------- inp : list or str create `AtomInput` from the content of `inp` """ def _get_content(f): if f.is_file(): return open(f, 'r').readlines() return None if isinstance(inp, (tuple, list)): # it is already in correct format pass elif isinstance(inp, (str, Path)): # convert to path inp = Path(inp) # Check if it is a path or an input content = _get_content(inp) if content is None: content = _get_content(inp / "INP") if content is None: raise ValueError( f"Could not find any input file in {str(inp)} or {str(inp / 'INP')}" ) inp = content else: raise ValueError(f"Unknown input format inp={inp}?") # Now read lines defines = [] opts = PropertyDict() def bypass_comments(inp): if inp[0].startswith("#"): inp.pop(0) bypass_comments(inp) def bypass(inp, defines): bypass_comments(inp) if inp[0].startswith("%define"): line = inp.pop(0) defines.append(line.split()[1].strip()) bypass(inp, defines) bypass(inp, defines) # Now prepare reading # First line has to contain the *type* of calculation # pg|pe|ae|pt <comment> line = inp.pop(0).strip() if line.startswith("pg"): opts.cc = False elif line.startswith("pe"): opts.cc = True # <flavor> logr? line = inp.pop(0).strip().split() opts.flavor = line[0] if len(line) >= 2: opts.logr = float(line[1]) / _Ang2Bohr # <element> <xc>' rs'? line = inp.pop(0) symbol = line.split()[0] # now get xc equation if len(line) >= 11: opts.equation = line[10:10] opts.xc = line[:10].split()[1] line = line.split() if len(line) >= 3: opts.libxc = int(line[2]) # currently not used line inp.pop(0) # core, valence core, valence = inp.pop(0).split() opts.core = int(core) valence = int(valence) orbs = [] for _ in range(valence): n, l, *occ = inp.pop(0).split() orb = PropertyDict() orb.n = int(n) orb.l = int(l) # currently we don't distinguish between up/down orb.q0 = sum(map(float, occ)) orbs.append(orb) # now we read the line with rc's and core-correction rcs = inp.pop(0).split() if len(rcs) >= 6: # core-correction opts.rcore = float(rcs[5]) / _Ang2Bohr for orb in orbs: orb.R = float(rcs[orb.l]) / _Ang2Bohr # Now create orbitals orbs = [si.AtomicOrbital(**orb, m=0, zeta=1) for orb in orbs] # now re-arrange ensuring we have correct order of l shells orbs = sorted(orbs, key=lambda orb: orb.l) atom = si.Atom(symbol, orbs) return cls(atom, defines, **opts)
def __init__(self, atom, define=('NEW_CC', 'FREE_FORMAT_RC_INPUT', 'NO_PS_CUTOFFS'), **opts): # opts = { # "flavor": "tm2", # "xc": "pb", # optionally libxc # "equation": "r", # "logr": 2. # "cc": False, # "rcore": 2. # } self.atom = atom assert isinstance(atom, si.Atom) if "." in self.atom.tag: raise ValueError("The atom 'tag' must not contain a '.'!") # We need to check that atom has 4 orbitals, with increasing l # We don't care about n or any other stuff, so these could be # SphericalOrbital, for that matter l = 0 for orb in self.atom: if orb.l != l: raise ValueError( f"{self.__class__.__name__} atom argument does not have " f"increasing l quantum number index {l} has l={orb.l}") l += 1 if l != 4: raise ValueError( f"{self.__class__.__name__} atom argument must have 4 orbitals. " f"One for each s-p-d-f shell") self.opts = PropertyDict(**opts) # Check options passed and define defaults self.opts.setdefault("equation", "r") if self.opts.equation not in ' rs': # ' ' == non-polarized # s == polarized # r == relativistic raise ValueError( f"{self.__class__.__name__} failed to initialize; opts{'equation': <v>} has wrong value, should be [ rs]." ) if self.opts.equation == 's': raise NotImplementedError( f"{self.__class__.__name__} does not implement spin-polarized option (use relativistic)" ) self.opts.setdefault("flavor", "tm2") if self.opts.flavor not in ('hsc', 'ker', 'tm2'): # hsc == Hamann-Schluter-Chiang # ker == Kerker # tm2 == Troullier-Martins raise ValueError( f"{self.__class__.__name__} failed to initialize; opts{'flavor': <v>} has wrong value, should be [hsc|ker|tm2]." ) self.opts.setdefault("logr", 2.) # default to true if set self.opts.setdefault("cc", "rcore" in self.opts) # rcore only used if cc is True self.opts.setdefault("rcore", 2.) self.opts.setdefault("xc", "pb") # Read in the core valence shells for this atom # figure out what the default value is. # We do this my finding the minimum index of valence shells # in the _shell_order list, then we use that as the default number # of core-shells occpupied # e.g if the minimum valence shell is 2p, it would mean that # _shell_order.index("2p") == 2 # which has 1s and 2s occupied. spdf = 'spdf' try: core = reduce(min, (_shell_order.index(f"{orb.n}{spdf[orb.l]}") for orb in atom), len(_shell_order)) except: core = -1 self.opts.setdefault("core", core) if self.opts.core == -1: raise ValueError( f"Default value for {self.atom.symbol} not added, please add core= at instantiation" ) # Store the defined names if define is None: self.define = [] elif isinstance(define, str): self.define = [define] else: # must be list-like self.define = define
def read_charge(self, name, iscf=Opt.ANY, imd=Opt.ANY, key_scf="scf", as_dataframe=False): r"""Read charges calculated in SCF loop or MD loop (or both) Siesta enables many different modes of writing out charges. NOTE: currently Mulliken charges are not implemented. The below table shows a list of different cases that may be encountered, the letters are referred to in the return section to indicate what is returned. +-----------+-----+-----+--------+-------+------------------+ | Case | *A* | *B* | *C* | *D* | *E* | +-----------+-----+-----+--------+-------+------------------+ | Charge | MD | SCF | MD+SCF | Final | Orbital resolved | +-----------+-----+-----+--------+-------+------------------+ | Voronoi | + | + | + | + | - | +-----------+-----+-----+--------+-------+------------------+ | Hirshfeld | + | + | + | + | - | +-----------+-----+-----+--------+-------+------------------+ | Mulliken | + | + | + | + | + | +-----------+-----+-----+--------+-------+------------------+ Notes ----- Errors will be raised if one requests information not present. I.e. passing an integer or `Opt.ALL` for `iscf` will raise an error if the SCF charges are not present. For `Opt.ANY` it will return the most information, effectively SCF will be returned if present. Currently Mulliken is not implemented, any help in reading this would be very welcome. Parameters ---------- name: {"voronoi", "hirshfeld"} the name of the charges that you want to read iscf: int or Opt, optional index (0-based) of the scf iteration you want the charges for. If the enum specifier `Opt.ANY` or `Opt.ALL` are used, then the returned quantities depend on what is present. If ``None/Opt.NONE`` it will not return any SCF charges. If both `imd` and `iscf` are ``None`` then only the final charges will be returned. imd: int or Opt, optional index (0-based) of the md step you want the charges for. If the enum specifier `Opt.ANY` or `Opt.ALL` are used, then the returned quantities depend on what is present. If ``None/Opt.NONE`` it will not return any MD charges. If both `imd` and `iscf` are ``None`` then only the final charges will be returned. key_scf : str, optional the key lookup for the scf iterations (a ":" will automatically be appended) as_dataframe: boolean, optional whether charges should be returned as a pandas dataframe. Returns ------- numpy.ndarray if a specific MD+SCF index is requested (or special cases where output is not complete) list of numpy.ndarray if one both `iscf` or `imd` is different from ``None/Opt.NONE``. pandas.DataFrame if `as_dataframe` is requested. The dataframe will have multi-indices if multiple SCF or MD steps are requested. """ if not hasattr(self, 'fh'): with self: return read_charge(self, name, iscf, imd, key_scf, as_dataframe) namel = name.lower() if as_dataframe: import pandas as pd def _empty_charge(): # build a fake dataframe with no indices return pd.DataFrame(index=pd.Index([], name="atom", dtype=np.int32), dtype=np.float32) else: pd = None def _empty_charge(): # return for single value with nan values return _a.arrayf([[None]]) # define helper function for reading voronoi+hirshfeld charges def _voronoi_hirshfeld_charges(): """ Read output from Voronoi/Hirshfeld charges """ nonlocal pd # Expecting something like this: # Voronoi Atomic Populations: # Atom # dQatom Atom pop S Sx Sy Sz Species # 1 -0.02936 4.02936 0.00000 -0.00000 0.00000 0.00000 C # Define the function that parses the charges def _parse_charge(line): atom_idx, *vals, symbol = line.split() # assert that this is a proper line # this should catch cases where the following line of charge output # is still parseable atom_idx = int(atom_idx) return list(map(float, vals)) # first line is the header header = ( self.readline().replace("dQatom", "dq") # dQatom in master .replace(" Qatom", " dq") # Qatom in 4.1 .replace("Atom pop", "e") # not found in 4.1 .split())[2:-1] # We have found the header, prepare a list to read the charges atom_charges = [] line = ' ' while line != "": try: line = self.readline() charge_vals = _parse_charge(line) atom_charges.append(charge_vals) except: # We already have the charge values and we reached a line that can't be parsed, # this means we have reached the end. break if pd is None: # not as_dataframe return _a.arrayf(atom_charges) # determine how many columns we have # this will remove atom indices and species, so only inside ncols = len(atom_charges[0]) assert ncols == len(header) # the precision is limited, so no need for double precision return pd.DataFrame(atom_charges, columns=header, dtype=np.float32, index=pd.RangeIndex(stop=len(atom_charges), name="atom")) # define helper function for reading voronoi+hirshfeld charges def _mulliken_charges(): """ Read output from Mulliken charges """ raise NotImplementedError( "Mulliken charges are not implemented currently") # Check that a known charge has been requested if namel == "voronoi": _r_charge = _voronoi_hirshfeld_charges charge_keys = [ "Voronoi Atomic Populations", "Voronoi Net Atomic Populations" ] elif namel == "hirshfeld": _r_charge = _voronoi_hirshfeld_charges charge_keys = [ "Hirshfeld Atomic Populations", "Hirshfeld Net Atomic Populations" ] elif namel == "mulliken": _r_charge = _mulliken_charges charge_keys = ["mulliken: Atomic and Orbital Populations"] else: raise ValueError( f"{self.__class__.__name__}.read_charge name argument should be one of {known_charges}, got {name}?" ) # Ensure the key_scf matches exactly (prepend a space) key_scf = f" {key_scf.strip()}:" # Reading charges may be quite time consuming for large MD simulations. # to see if we finished a MD read, we check for these keys search_keys = [ # two keys can signal ending SCF "SCF Convergence", "SCF_NOT_CONV", "siesta: Final energy", key_scf, *charge_keys ] # adjust the below while loop to take into account any additional # segments of search_keys IDX_SCF_END = [0, 1] IDX_FINAL = [2] IDX_SCF = [3] # the rest are charge keys IDX_CHARGE = list( range(len(search_keys) - len(charge_keys), len(search_keys))) # state to figure out where we are state = PropertyDict() state.INITIAL = 0 state.MD = 1 state.SCF = 2 state.CHARGE = 3 state.FINAL = 4 # a list of scf_charge md_charge = [] md_scf_charge = [] scf_charge = [] final_charge = None # signal that any first reads are INITIAL charges current_state = state.INITIAL charge = _empty_charge() FOUND_SCF = False FOUND_MD = False FOUND_FINAL = False # TODO whalrus ret = self.step_to(search_keys, case=True, ret_index=True, reread=False) while ret[0]: if ret[2] in IDX_SCF_END: # we finished all SCF iterations current_state = state.MD md_scf_charge.append(scf_charge) scf_charge = [] elif ret[2] in IDX_SCF: current_state = state.SCF # collect scf-charges (possibly none) scf_charge.append(charge) elif ret[2] in IDX_FINAL: current_state = state.FINAL # don't do anything, this is the final charge construct # regardless of where it comes from. elif ret[2] in IDX_CHARGE: FOUND_CHARGE = True # also read charge charge = _r_charge() if state.INITIAL == current_state or state.CHARGE == current_state: # this signals scf charges FOUND_SCF = True # There *could* be 2 steps if we are mixing H, # this is because it first does # compute H -> compute DM -> compute H # in the first iteration, subsequently we only do # compute compute DM -> compute H # once we hit ret[2] in IDX_SCF we will append scf_charge = [] elif state.MD == current_state: FOUND_MD = True # we just finished an SCF cycle. # So any output between SCF ending and # a new one beginning *must* be that geometries # charge # Here `charge` may be NONE signalling # we don't have charge in MD steps. md_charge.append(charge) # reset charge charge = _empty_charge() elif state.SCF == current_state: FOUND_SCF = True elif state.FINAL == current_state: FOUND_FINAL = True # a special state writing out the charges after everything final_charge = charge charge = _empty_charge() scf_charge = [] # we should be done and no other charge reads should be found! # should we just break? current_state = state.CHARGE # step to next entry ret = self.step_to(search_keys, case=True, ret_index=True, reread=False) if not any((FOUND_SCF, FOUND_MD, FOUND_FINAL)): raise SileError( f"{str(self)} does not contain any charges ({name})") # if the scf-charges are not stored, it means that the MD step finalization # has not been read. So correct if len(scf_charge) > 0: assert False, "this test shouldn't reach here" # we must not have read through the entire MD step # so this has to be a running simulation if charge is not None: scf_charge.append(charge) charge = _empty_charge() md_scf_charge.append(scf_charge) # otherwise there is some *parsing* error, so for now we use assert assert len(scf_charge) == 0 if as_dataframe: # convert data to proper data structures # regardless of user requests. This is an overhead... But probably not that big of a problem. if FOUND_SCF: md_scf_charge = pd.concat([ pd.concat(iscf, keys=pd.RangeIndex(1, len(iscf) + 1, name="iscf")) for iscf in md_scf_charge ], keys=pd.RangeIndex( 1, len(md_scf_charge) + 1, name="imd")) if FOUND_MD: md_charge = pd.concat(md_charge, keys=pd.RangeIndex(1, len(md_charge) + 1, name="imd")) else: if FOUND_SCF: nan_array = _a.emptyf(md_scf_charge[0][0].shape) nan_array.fill(np.nan) def get_md_scf_charge(scf_charge, iscf): try: return scf_charge[iscf] except: return nan_array if FOUND_MD: md_charge = np.stack(md_charge) # option parsing is a bit *difficult* with flag enums # So first figure out what is there, and handle this based # on arguments def _p(flag, found): """ Helper routine to do the following: Returns ------- is_opt : bool whether the flag is an `Opt` flag : corrected flag """ if isinstance(flag, Opt): # correct flag depending on what `found` is # If the values have been found we # change flag to None only if flag == NONE # If the case has not been found, we # change flag to None if ANY or NONE is in flags if found: # flag is only NONE, then pass none if not (Opt.NONE ^ flag): flag = None else: # not found # we convert flag to none # if ANY or NONE in flag if (Opt.NONE | Opt.ANY) & flag: flag = None return isinstance(flag, Opt), flag opt_imd, imd = _p(imd, FOUND_MD) opt_iscf, iscf = _p(iscf, FOUND_SCF) if not (FOUND_SCF or FOUND_MD): # none of these are found # we request that user does not request any input if (opt_iscf or (not iscf is None)) or \ (opt_imd or (not imd is None)): raise SileError(f"{str(self)} does not contain MD/SCF charges") elif not FOUND_SCF: if opt_iscf or (not iscf is None): raise SileError(f"{str(self)} does not contain SCF charges") elif not FOUND_MD: if opt_imd or (not imd is None): raise SileError(f"{str(self)} does not contain MD charges") # if either are options they may hold if opt_imd and opt_iscf: if FOUND_SCF: return md_scf_charge elif FOUND_MD: return md_charge elif FOUND_FINAL: # I think this will never be reached # If neither are found they will be converted to # None return final_charge raise SileError( f"{str(self)} unknown argument for 'imd' and 'iscf'") elif opt_imd: # flag requested imd if not (imd & (Opt.ANY | Opt.ALL)): # wrong flag raise SileError(f"{str(self)} unknown argument for 'imd'") if FOUND_SCF and iscf is not None: # this should be handled, i.e. the scf should be taken out if as_dataframe: return md_scf_charge.groupby(level=[0, 2]).nth(iscf) return np.stack( tuple(get_md_scf_charge(x, iscf) for x in md_scf_charge)) elif FOUND_MD and iscf is None: return md_charge raise SileError( f"{str(self)} unknown argument for 'imd' and 'iscf', could not find SCF charges" ) elif opt_iscf: # flag requested imd if not (iscf & (Opt.ANY | Opt.ALL)): # wrong flag raise SileError(f"{str(self)} unknown argument for 'iscf'") if imd is None: # correct imd imd = -1 if as_dataframe: md_scf_charge = md_scf_charge.groupby(level=0) group = list(md_scf_charge.groups.keys())[imd] return md_scf_charge.get_group(group).droplevel(0) return np.stack(md_scf_charge[imd]) elif imd is None and iscf is None: if FOUND_FINAL: return final_charge raise SileError(f"{str(self)} does not contain final charges") elif imd is None: # iscf is not None, so pass through as though explicitly passed imd = -1 elif iscf is None: # we return the last MD step and the requested scf iteration if as_dataframe: return md_charge.groupby(level=1).nth(imd) return md_charge[imd] if as_dataframe: # first select imd md_scf_charge = md_scf_charge.groupby(level=0) group = list(md_scf_charge.groups.keys())[imd] md_scf_charge = md_scf_charge.get_group(group).droplevel(0) return md_scf_charge.groupby(level=1).nth(iscf) return md_scf_charge[imd][iscf]
def read_energy(self): """ Reads the final energy distribution Currently the energies translated are: ``band`` band structure energy ``kinetic`` electronic kinetic energy ``hartree`` electronic electrostatic Hartree energy ``dftu`` DFT+U energy ``spin_orbit`` spin-orbit energy ``extE`` external field energy ``xc`` exchange-correlation energy ``exchange`` exchange energy ``correlation`` correlation energy ``bulkV`` bulk-bias correction energy ``total`` total energy ``negf`` NEGF energy ``fermi`` Fermi energy ``ion.electron`` ion-electron interaction energy ``ion.ion`` ion-ion interaction energy ``ion.kinetic`` kinetic ion energy Any unrecognized key gets added *as is*. Examples -------- >>> energies = sisl.get_sile("RUN.out").read_energy() >>> ion_energies = energies.ion >>> ion_energies.ion # ion-ion interaction energy >>> ion_energies.kinetic # ion kinetic energy >>> energies.fermi # fermi energy Returns ------- PropertyDict : dictionary like lookup table ionic energies are stored in a nested `PropertyDict` at the key ``ion`` (all energies in eV) """ found = self.step_to("siesta: Final energy", reread=False)[0] out = PropertyDict() out.ion = PropertyDict() if not found: return out itt = iter(self) # Read data line = next(itt) name_conv = { "Band Struct.": "band", "Kinetic": "kinetic", "Hartree": "hartree", "Edftu": "dftu", "Eldau": "dftu", "Eso": "spin_orbit", "Ext. field": "extE", "Exch.-corr.": "xc", "Exch.": "exchange", "Corr.": "correlation", "Ekinion": "ion.kinetic", "Ion-electron": "ion.electron", "Ion-ion": "ion.ion", "Bulk bias": "bulkV", "Total": "total", "Fermi": "fermi", "Enegf": "negf", } while len(line.strip()) > 0: key, val = line.split("=") key = key.split(":")[1].strip() key = name_conv.get(key, key) if key.startswith("ion."): # sub-nest out.ion[key[4:]] = float(val) else: out[key] = float(val) line = next(itt) return out
def ArgumentParser(self, p=None, *args, **kwargs): """ Returns the arguments that is available for this Sile """ #limit_args = kwargs.get('limit_arguments', True) short = kwargs.get('short', False) def opts(*args): if short: return args return [args[0]] # We limit the import to occur here import argparse Bohr2Ang = unit_convert('Bohr', 'Ang') Ry2eV = unit_convert('Bohr', 'Ang') # The first thing we do is adding the geometry to the NameSpace of the # parser. # This will enable custom actions to interact with the geometry in a # straight forward manner. # convert netcdf file to a dictionary ion_nc = PropertyDict() ion_nc.n = self._variable('orbnl_n')[:] ion_nc.l = self._variable('orbnl_l')[:] ion_nc.zeta = self._variable('orbnl_z')[:] ion_nc.pol = self._variable('orbnl_ispol')[:] ion_nc.orbital = self._variable('orb')[:] # this gets converted later delta = self._variable('delta')[:] r = aranged(ion_nc.orbital.shape[1]).reshape(1, -1) * delta.reshape(-1, 1) ion_nc.orbital *= r ** ion_nc.l.reshape(-1, 1) / Bohr2Ang * (3./2.) ion_nc.r = r * Bohr2Ang ion_nc.kb = PropertyDict() ion_nc.kb.n = self._variable('pjnl_n')[:] ion_nc.kb.l = self._variable('pjnl_l')[:] ion_nc.kb.e = self._variable('pjnl_ekb')[:] * Ry2eV ion_nc.kb.proj = self._variable('proj')[:] delta = self._variable('kbdelta')[:] r = aranged(ion_nc.kb.proj.shape[1]).reshape(1, -1) * delta.reshape(-1, 1) ion_nc.kb.proj *= r ** ion_nc.kb.l.reshape(-1, 1) / Bohr2Ang * (3./2.) ion_nc.kb.r = r * Bohr2Ang vna = self._variable('vna') r = aranged(vna[:].size) * vna.Vna_delta ion_nc.vna = PropertyDict() ion_nc.vna.v = vna[:] * Ry2eV * r / Bohr2Ang ** 3 ion_nc.vna.r = r * Bohr2Ang # this is charge (not 1/sqrt(charge)) chlocal = self._variable('chlocal') r = aranged(chlocal[:].size) * chlocal.Chlocal_delta ion_nc.chlocal = PropertyDict() ion_nc.chlocal.v = chlocal[:] * r / Bohr2Ang ** 3 ion_nc.chlocal.r = r * Bohr2Ang vlocal = self._variable('reduced_vlocal') r = aranged(vlocal[:].size) * vlocal.Reduced_vlocal_delta ion_nc.vlocal = PropertyDict() ion_nc.vlocal.v = vlocal[:] * r / Bohr2Ang ** 3 ion_nc.vlocal.r = r * Bohr2Ang if "core" in self.variables: # this is charge (not 1/sqrt(charge)) core = self._variable('core') r = aranged(core[:].size) * core.Core_delta ion_nc.core = PropertyDict() ion_nc.core.v = core[:] * r / Bohr2Ang ** 3 ion_nc.core.r = r * Bohr2Ang d = { "_data": ion_nc, "_kb_proj": False, "_l": True, "_n": True, } namespace = default_namespace(**d) # l-quantum number class lRange(argparse.Action): def __call__(self, parser, ns, value, option_string=None): value = (value .replace("s", 0) .replace("p", 1) .replace("d", 2) .replace("f", 3) .replace("g", 4) ) ns._l = strmap(int, value)[0] p.add_argument('-l', action=lRange, help='Denote the sub-section of l-shells that are plotted: "s,f"') # n quantum number class nRange(argparse.Action): def __call__(self, parser, ns, value, option_string=None): ns._n = strmap(int, value)[0] p.add_argument('-n', action=nRange, help='Denote the sub-section of n quantum numbers that are plotted: "2-4,6"') class Plot(argparse.Action): def __call__(self, parser, ns, value, option_string=None): import matplotlib.pyplot as plt # Retrieve values data = ns._data # We have these plots: # - orbitals # - projectors # - chlocal # - vna # - vlocal # - core (optional) # We'll plot them like this: # orbitals | projectors # vna + vlocal | chlocal + core # # Determine different n, l fig, axs = plt.subplots(2, 2) # Now plot different orbitals for n, l, zeta, pol, r, orb in zip(data.n, data.l, data.zeta, data.pol, data.r, data.orbital): if pol == 1: pol = 'P' else: pol = '' axs[0][0].plot(r, orb, label=f"n{n}l{l}Z{zeta}{pol}") axs[0][0].set_title("Orbitals") axs[0][0].set_xlabel("Distance [Ang]") axs[0][0].set_ylabel("Value [a.u.]") axs[0][0].legend() # plot projectors for n, l, e, r, proj in zip( data.kb.n, data.kb.l, data.kb.e, data.kb.r, data.kb.proj): axs[0][1].plot(r, proj, label=f"n{n}l{l} e={e:.5f}") axs[0][1].set_title("KB projectors") axs[0][1].set_xlabel("Distance [Ang]") axs[0][1].set_ylabel("Value [a.u.]") axs[0][1].legend() axs[1][0].plot(data.vna.r, data.vna.v, label='Vna') axs[1][0].plot(data.vlocal.r, data.vlocal.v, label='Vlocal') axs[1][0].set_title("Potentials") axs[1][0].set_xlabel("Distance [Ang]") axs[1][0].set_ylabel("Potential [eV]") axs[1][0].legend() axs[1][1].plot(data.chlocal.r, data.chlocal.v, label='Chlocal') if "core" in data: axs[1][1].plot(data.core.r, data.core.v, label='core') axs[1][1].set_title("Charge") axs[1][1].set_xlabel("Distance [Ang]") axs[1][1].set_ylabel("Charge [Ang^3]") axs[1][1].legend() if value is None: plt.show() else: plt.savefig(value) p.add_argument(*opts('--plot', '-p'), action=Plot, nargs='?', metavar='FILE', help='Plot the content basis set file, possibly saving plot to a file.') return p, namespace
def candidates(self, delta=1e-2, target=None, sort="max"): """ Compare samples and find candidates within a delta-metric of `delta` Candidiates are ordered around the basis-set sizes. This means that *zeta* variables are the only ones used for figuring out the candidates. Parameters ---------- delta : float, optional only consider sampled metrics that lie within ``target +- delta`` target : float, optional target metric value to search around. This may be useful in situations where all basis sets are very large and that accuracy isn't really needed. Defaults to the minimum metric. sort : {max, l1, l2}, callable How to sort the basis set ranges. If a callable it should return the indices that pivots the data to sorted candidates. The callable should accept two arrays, ``(x, y)`` and all variables will be passed (not only basis-set ranges). """ # Retrieve all data points within the minim x = np.array(self.data.x) y = np.array(self.data.y) if target is None: idx_target = np.argmin(y) else: idx_target = np.argmin(np.fabs(y - target)) xtarget = x[idx_target] ytarget = y[idx_target] # Now find all valid samples valid = np.logical_and(ytarget - delta <= y, y <= ytarget + delta).nonzero()[0] # Reduce to candidate points x_valid = x[valid] y_valid = y[valid] # Figure out which variables are *basis ranges* idx_R = [] for idx, v in enumerate(self.variables): # I think this should be enough for a zeta value if ".z" in v.name: idx_R.append(idx) if len(idx_R) > 0: # only use these indices to find minimum candidates if isinstance(sort, str): if sort == "max": idx_increasing = np.argsort(x_valid[:, idx_R].max(axis=1)) elif sort == "l1": idx_increasing = np.argsort(x_valid[:, idx_R].sum(axis=1)) elif sort == "l2": # no need for sqrt (does nothing for sort) idx_increasing = np.argsort( (x_valid[:, idx_R]**2).sum(axis=1)) else: raise ValueError( f"{self.__class__.__name__}.candidates got an unknown value for 'sort={sort}', must be one of [max,l1,l2]." ) else: # it really has to be callable ;) idx_increasing = sort(x_valid, y_valid) x_valid = x_valid[idx_increasing] y_valid = y_valid[idx_increasing] elif callable(sort): idx_increasing = sort(x_valid, y_valid) x_valid = x_valid[idx_increasing] y_valid = y_valid[idx_increasing] # Return the candidates candidates = PropertyDict() candidates.x = x_valid candidates.y = y_valid candidates.x_target = xtarget candidates.y_target = ytarget return candidates
# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. """ Default graphene models """ from sisl.utils import PropertyDict # Here we import the specific details that are exposed from ._hamiltonian import * __all__ = ['graphene'] # Define the graphene model graphene = PropertyDict() graphene.hamiltonian = GrapheneHamiltonian() graphene.H = graphene.hamiltonian