# coding: utf-8
"""Classes for the analysis of GW calculations."""
import sys
import copy
import warnings
import numpy as np
import pandas as pd
from collections import namedtuple, OrderedDict
from io import StringIO
from tabulate import tabulate
from monty.string import list_strings, is_string, marquee
from monty.collections import dict2namedtuple
from monty.functools import lazy_property
from monty.termcolor import cprint
from monty.bisect import find_le, find_ge
from abipy.core.func1d import Function1D
from abipy.core.kpoints import Kpoint, KpointList, Kpath, IrredZone, has_timrev_from_kptopt
from abipy.core.mixins import AbinitNcFile, Has_Structure, Has_ElectronBands, NotebookWriter
from abipy.iotools import ETSF_Reader
from abipy.tools.plotting import (ArrayPlotter, add_fig_kwargs, get_ax_fig_plt, get_axarray_fig_plt, Marker,
set_axlims, set_visible, rotate_ticklabels)
from abipy.tools import duck
from abipy.abio.robots import Robot
from abipy.electrons.ebands import ElectronBands, RobotWithEbands
from abipy.electrons.scissors import Scissors
import logging
logger = logging.getLogger(__name__)
__all__ = [
"QPState",
"SigresFile",
"SigresRobot",
]
[docs]class QPState(namedtuple("QPState", "spin kpoint band e0 qpe qpe_diago vxcme sigxme sigcmee0 vUme ze0")):
"""
Quasi-particle result for given (spin, kpoint, band).
.. Attributes:
spin: spin index (C convention, i.e >= 0)
kpoint: |Kpoint| object.
band: band index. (C convention, i.e >= 0).
e0: Initial KS energy.
qpe: Quasiparticle energy (complex) computed with the perturbative approach.
qpe_diago: Quasiparticle energy (real) computed by diagonalizing the self-energy.
vxcme: Matrix element of vxc[n_val] with nval the valence charge density.
sigxme: Matrix element of Sigma_x.
sigcmee0: Matrix element of Sigma_c(e0) with e0 being the KS energy.
vUme: Matrix element of the vU term of the LDA+U Hamiltonian.
ze0: Renormalization factor computed at e=e0.
.. note::
Energies are in eV.
"""
@property
def qpeme0(self):
"""E_QP - E_0"""
return self.qpe - self.e0
@property
def re_qpe(self):
"""Real part of the QP energy."""
return self.qpe.real
@property
def imag_qpe(self):
"""Imaginay part of the QP energy."""
return self.qpe.imag
@property
def skb(self):
"""Tuple with (spin, kpoint, band)"""
return self.spin, self.kpoint, self.band
#def __str__(self):
# return self.to_string()
#def to_string(self, verbose=0, title=None):
# """
# String representation with verbosity level ``verbose`` and optional ``title``.
# """
# s = str(self.get_dataframe())
# return "\n".join([marquee(title, mark="="), s]) if title is not None else s
[docs] def copy(self):
"""Return Shallow copy."""
d = {f: copy.copy(getattr(self, f)) for f in self._fields}
return self.__class__(**d)
[docs] @classmethod
def get_fields(cls, exclude=()):
fields = list(cls._fields) + ["qpeme0"]
for e in exclude:
fields.remove(e)
return tuple(fields)
[docs] def as_dict(self, **kwargs):
"""
Convert self into a dictionary.
"""
od = OrderedDict(zip(self._fields, self))
od["qpeme0"] = self.qpeme0
return od
[docs] def to_strdict(self, fmt=None):
"""
Ordered dictionary mapping fields --> strings.
"""
d = self.as_dict()
for k, v in d.items():
if duck.is_intlike(v):
d[k] = "%d" % int(v)
elif isinstance(v, Kpoint):
d[k] = "%s" % v
elif np.iscomplexobj(v):
if abs(v.imag) < 1.e-3:
d[k] = "%.2f" % v.real
else:
d[k] = "%.2f%+.2fj" % (v.real, v.imag)
else:
try:
d[k] = "%.2f" % v
except TypeError as exc:
#print("k", k, str(exc))
d[k] = str(v)
return d
@property
def tips(self):
"""Bound method of self that returns a dictionary with the description of the fields."""
return self.__class__.TIPS()
[docs] @classmethod
def TIPS(cls):
"""
Class method that returns a dictionary with the description of the fields.
The string are extracted from the class doc string.
"""
try:
return cls._TIPS
except AttributeError:
# Parse the doc string.
cls._TIPS = _TIPS = {}
lines = cls.__doc__.splitlines()
for i, line in enumerate(lines):
if line.strip().startswith(".. Attributes"):
lines = lines[i+1:]
break
def num_leadblanks(string):
"""Returns the number of the leading whitespaces."""
return len(string) - len(string.lstrip())
for field in cls._fields:
for i, line in enumerate(lines):
if line.strip().startswith(field + ":"):
nblanks = num_leadblanks(line)
desc = []
for s in lines[i+1:]:
if nblanks == num_leadblanks(s) or not s.strip():
break
desc.append(s.lstrip())
_TIPS[field] = "\n".join(desc)
diffset = set(cls._fields) - set(_TIPS.keys())
if diffset:
raise RuntimeError("The following fields are not documented: %s" % str(diffset))
return _TIPS
[docs] @classmethod
def get_fields_for_plot(cls, with_fields, exclude_fields):
"""
Return list of QPState fields to plot from input arguments.
"""
all_fields = list(cls.get_fields(exclude=["spin", "kpoint"]))[:]
# Initialize fields.
if is_string(with_fields) and with_fields == "all":
fields = all_fields
else:
fields = list_strings(with_fields)
for f in fields:
if f not in all_fields:
raise ValueError("Field %s not in allowed values %s" % (f, all_fields))
# Remove entries
if exclude_fields:
if is_string(exclude_fields):
exclude_fields = exclude_fields.split()
for e in exclude_fields:
try:
fields.remove(e)
except ValueError:
pass
return fields
class QPList(list):
"""
A list of quasiparticle corrections for a given spin.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args)
self.is_e0sorted = kwargs.get("is_e0sorted", False)
def __repr__(self):
return "<%s at %s, len=%d>" % (self.__class__.__name__, id(self), len(self))
def __str__(self):
"""String representation."""
return self.to_string()
def to_table(self):
"""Return a table (list of list of strings)."""
header = QPState.get_fields(exclude=["spin", "kpoint"])
table = [header]
for qp in self:
d = qp.to_strdict(fmt=None)
table.append([d[k] for k in header])
return tabulate(table, tablefmt="plain")
def to_string(self, verbose=0):
"""String representation."""
return self.to_table()
def copy(self):
"""Copy of self."""
return self.__class__([qp.copy() for qp in self], is_e0sorted=self.is_e0sorted)
def sort_by_e0(self):
"""Return a new object with the E0 energies sorted in ascending order."""
return self.__class__(sorted(self, key=lambda qp: qp.e0), is_e0sorted=True)
def get_e0mesh(self):
"""Return the E0 energies."""
if not self.is_e0sorted:
raise ValueError("QPState corrections are not sorted. Use sort_by_e0")
return np.array([qp.e0 for qp in self])
def get_field(self, field):
"""|numpy-array| containing the values of field."""
return np.array([getattr(qp, field) for qp in self])
def get_skb_field(self, skb, field):
"""Return the value of field for the given spin kp band tuple, None if not found"""
for qp in self:
if qp.skb == skb:
return getattr(qp, field)
return None
def get_qpenes(self):
"""Return an array with the :class:`QPState` energies."""
return self.get_field("qpe")
def get_qpeme0(self):
"""Return an arrays with the :class:`QPState` corrections."""
return self.get_field("qpeme0")
def merge(self, other, copy=False):
"""
Merge self with other. Return new :class:`QPList` object
Raise:
`ValueError` if merge cannot be done.
"""
skb0_list = [qp.skb for qp in self]
for qp in other:
if qp.skb in skb0_list:
raise ValueError("Found duplicated (s,b,k) indexes: %s" % str(qp.skb))
qps = self.copy() + other.copy() if copy else self + other
return self.__class__(qps)
@add_fig_kwargs
def plot_qps_vs_e0(self, with_fields="all", exclude_fields=None, fermie=None,
ax_list=None, sharey=False, xlims=None, fontsize=12, **kwargs):
"""
Plot the QP results as function of the initial KS energy.
Args:
with_fields: The names of the qp attributes to plot as function of e0.
Accepts: List of strings or string with tokens separated by blanks.
See :class:`QPState` for the list of available fields.
exclude_fields: Similar to ``with_field`` but excludes fields.
fermie: Value of the Fermi level used in plot. None for absolute e0s.
ax_list: List of |matplotlib-Axes| for plot. If None, new figure is produced.
sharey: True if y-axis should be shared.
kwargs: linestyle, color, label, marker
Returns: |matplotlib-Figure|
"""
# TODO: This is to maintain the old API.
fermi = kwargs.pop("fermi", None)
if fermi is not None:
fermie = fermi
warnings.warn("fermi keyword argument have been renamed fermie. Old arg will be removed in version 0.4")
fields = QPState.get_fields_for_plot(with_fields, exclude_fields)
if not fields: return None
num_plots, ncols, nrows = len(fields), 1, 1
if num_plots > 1:
ncols = 2
nrows = (num_plots // ncols) + (num_plots % ncols)
# Build grid of plots.
ax_list, fig, plt = get_axarray_fig_plt(ax_list, nrows=nrows, ncols=ncols,
sharex=True, sharey=sharey, squeeze=False)
ax_list = np.array(ax_list).ravel()
# Get qplist and sort it.
qps = self if self.is_e0sorted else self.sort_by_e0()
e0mesh = qps.get_e0mesh()
xlabel = r"$\epsilon_{KS}\;(eV)$"
#print("fermie", fermie)
if fermie is not None:
xlabel = r"$\epsilon_{KS}-\epsilon_F\;(eV)$"
e0mesh -= fermie
kw_linestyle = kwargs.pop("linestyle", "o")
kw_color = kwargs.pop("color", None)
kw_label = kwargs.pop("label", None)
for ii, (field, ax) in enumerate(zip(fields, ax_list)):
irow, icol = divmod(ii, ncols)
ax.grid(True)
if irow == nrows - 1:
ax.set_xlabel(xlabel)
ax.set_ylabel(field, fontsize=fontsize)
yy = qps.get_field(field)
# TODO real and imag?
ax.plot(e0mesh, yy.real, kw_linestyle, color=kw_color, label=kw_label, **kwargs)
set_axlims(ax, xlims, "x")
if kw_label:
ax_list[0].legend(loc="best", fontsize=fontsize, shadow=True)
# Get around a bug in matplotlib
if num_plots % ncols != 0: ax_list[-1].axis('off')
return fig
def build_scissors(self, domains, bounds=None, k=3, plot=False, **kwargs):
"""
Construct a scissors operator by interpolating the QPState corrections
as function of the initial energies E0.
Args:
domains: list in the form [ [start1, stop1], [start2, stop2]
Domains should not overlap, cover e0mesh, and given in increasing order.
Holes are permitted but the interpolation will raise an exception if
the point is not in domains.
bounds: Specify how to handle out-of-boundary conditions, i.e. how to treat
energies that do not fall inside one of the domains (not used at present)
plot: If true, use matplolib plot to compare input data and fit.
Return:
instance of :class:`Scissors`operator
Usage example:
.. code-block:: python
# Build the scissors operator.
scissors = qplist_spin[0].build_scissors(domains)
# Compute list of interpolated QP energies.
qp_enes = [scissors.apply(e0) for e0 in ks_energies]
"""
# Sort QP corrections according to the initial KS energy.
qps = self.sort_by_e0()
e0mesh, qpcorrs = qps.get_e0mesh(), qps.get_qpeme0()
# Check domains.
domains = np.atleast_2d(domains)
dsize, dflat = domains.size, domains.ravel()
for idx, v in enumerate(dflat):
if idx == 0 and v > e0mesh[0]:
raise ValueError("min(e0mesh) %s is not included in domains" % e0mesh[0])
if idx == dsize-1 and v < e0mesh[-1]:
raise ValueError("max(e0mesh) %s is not included in domains" % e0mesh[-1])
if idx != dsize-1 and dflat[idx] > dflat[idx+1]:
raise ValueError("domain boundaries should be given in increasing order.")
if idx == dsize-1 and dflat[idx] < dflat[idx-1]:
raise ValueError("domain boundaries should be given in increasing order.")
# Create the sub_domains and the spline functions in each subdomain.
func_list, residues = [], []
if len(domains) == 2:
#print('forcing extremal point on the scissor')
ndom = 0
else:
ndom = 99
for dom in domains[:]:
ndom += 1
low, high = dom[0], dom[1]
start, stop = find_ge(e0mesh, low), find_le(e0mesh, high)
dom_e0 = e0mesh[start:stop+1]
dom_corr = qpcorrs[start:stop+1]
# todo check if the number of non degenerate data points > k
from scipy.interpolate import UnivariateSpline
w = len(dom_e0)*[1]
if ndom == 1:
w[-1] = 1000
elif ndom == 2:
w[0] = 1000
else:
w = None
f = UnivariateSpline(dom_e0, dom_corr, w=w, bbox=[None, None], k=k, s=None)
func_list.append(f)
residues.append(f.get_residual())
# Build the scissors operator.
sciss = Scissors(func_list, domains, residues, bounds)
# Compare fit with input data.
if plot:
title = kwargs.pop("title", None)
import matplotlib.pyplot as plt
plt.plot(e0mesh, qpcorrs, 'o', label="input data")
if title: plt.suptitle(title)
for dom in domains[:]:
plt.plot(2*[dom[0]], [min(qpcorrs), max(qpcorrs)])
plt.plot(2*[dom[1]], [min(qpcorrs), max(qpcorrs)])
intp_qpc = [sciss.apply(e0) for e0 in e0mesh]
plt.plot(e0mesh, intp_qpc, label="scissor")
plt.legend(bbox_to_anchor=(0.9, 0.2))
plt.show()
# Return the object.
return sciss
class SelfEnergy(object):
"""
This object stores the values of the electron-electron self-energy
and the associated spectral function as function of frequency
"""
# Symbols used in matplotlib plots.
latex_symbol = dict(
re=r"$\Re{\Sigma(\omega)}$",
im=r"$\Im{\Sigma(\omega)}$",
spfunc=r"$A(\omega)}$",
)
def __init__(self, spin, kpoint, band, wmesh, sigmaxc_values, spfunc_values):
self.spin, self.kpoint, self.band = spin, kpoint, band
self.wmesh = np.array(wmesh)
self.xc = Function1D(self.wmesh, sigmaxc_values)
self.spfunc = Function1D(self.wmesh, spfunc_values)
assert len(sigmaxc_values) == len(spfunc_values)
def __str__(self):
return self.to_string()
def to_string(self, verbose=0, title=None):
"""String representation."""
lines = []; app = lines.append
if title is not None: app(marquee(title, mark="="))
app("K-point: %s, band: %d, spin: %d" % (repr(self.kpoint), self.band, self.spin))
app("Number of frequencies: %d, from %.1f to %.1f (eV)" % (len(self.wmesh), self.wmesh[0], self.wmesh[-1]))
return "\n".join(lines)
def plot_ax(self, ax, what="a", fontsize=12, **kwargs):
"""
Helper function to plot data on the axis ax with fontsize
Args:
ax: |matplotlib-Axes| or None if a new figure should be created.
what:
fontsize: legend and title fontsize.
Return: List of lines.
"""
#if not kwargs: kwargs = {"color": "black", "linewidth": 2.0}
lines = []
extend = lines.extend
ax.grid(True)
if what == "s":
f = self.xc
label = kwargs.get("label", r"$\Sigma(\omega)$")
extend(f.plot_ax(ax, cplx_mode="re", label="Re " + label))
extend(f.plot_ax(ax, cplx_mode="im", label="Im " + label))
#ax.set_ylabel('Energy (eV)')
elif what == "a":
f = self.spfunc
label = kwargs.get("label", r"$A(\omega)$")
extend(f.plot_ax(ax, label=label))
else:
raise ValueError("Don't know how to handle what option %s" % str(what))
#ax.set_xlabel(r"$\omega - \espilon_{KS} (eV)")
ax.legend(loc="best", fontsize=fontsize, shadow=True)
return lines
@add_fig_kwargs
def plot(self, ax_list=None, what_list=("re", "im", "spfunc"), fermie=None,
xlims=None, fontsize=12, **kwargs):
"""
Plot the real/imaginary part of self-energy as well as the spectral function
Args:
ax_list: List of |matplotlib-Axes| for plot. If None, new figure is produced.
what_list: List of strings selecting the quantity to plot.
fermie: Value of the Fermi level used in plot. None for absolute e0s.
xlims: Set the data limits for the x-axis. Accept tuple e.g. ``(left, right)``
or scalar e.g. ``left``. If left (right) is None, default values are used.
fontsize: legend and label fontsize.
kwargs: Keyword arguments passed to ax.plot
Returns: |matplotlib-Figure|
"""
what_list = list_strings(what_list)
ax_list, fig, plt = get_axarray_fig_plt(ax_list, nrows=len(what_list), ncols=1,
sharex=True, sharey=False, squeeze=False)
ax_list = np.array(ax_list).ravel()
xlabel = r"$\omega\;(eV)$"
wmesh = self.wmesh
if fermie is not None:
xlabel = r"$\omega - \epsilon_F}\;(eV)$"
wmesh = self.wmesh - fermie
kw_color = kwargs.pop("color", None)
kw_label = kwargs.pop("label", None)
for i, (what, ax) in enumerate(zip(what_list, ax_list)):
ax.grid(True)
ax.set_ylabel(self.latex_symbol[what])
if (i == len(ax_list) - 1): ax.set_xlabel(xlabel)
ax.plot(wmesh, self._get_ys(what),
color=kw_color,
label=kw_label if i == 0 else None,
)
set_axlims(ax, xlims, "x")
if i == 0 and kw_label:
ax.legend(loc="best", shadow=True, fontsize=fontsize)
if "title" not in kwargs:
title = "K-point: %s, band: %d, spin: %d" % (repr(self.kpoint), self.band, self.spin)
fig.suptitle(title, fontsize=fontsize)
return fig
def _get_ys(self, what):
"""Return array to plot from what string."""
return dict(
re=self.xc.values.real,
im=self.xc.values.imag,
spfunc=self.spfunc.values,
)[what]
[docs]class SigresFile(AbinitNcFile, Has_Structure, Has_ElectronBands, NotebookWriter):
"""
Container storing the GW results reported in the SIGRES.nc file.
Usage example:
.. code-block:: python
sigres = SigresFile("foo_SIGRES.nc")
sigres.plot_qps_vs_e0()
.. rubric:: Inheritance Diagram
.. inheritance-diagram:: SigresFile
"""
# Markers used for up/down bands.
marker_spin = {0: "^", 1: "v"}
color_spin = {0: "k", 1: "r"}
[docs] @classmethod
def from_file(cls, filepath):
"""Initialize an instance from file."""
return cls(filepath)
def __init__(self, filepath):
"""Read data from the netcdf file path."""
super().__init__(filepath)
# Keep a reference to the SigresReader.
self.reader = reader = SigresReader(self.filepath)
self._structure = reader.read_structure()
self.gwcalctyp = reader.gwcalctyp
self.ibz = reader.ibz
self.gwkpoints = reader.gwkpoints
self.nkcalc = len(self.gwkpoints)
self.gwbstart_sk = reader.gwbstart_sk
self.gwbstop_sk = reader.gwbstop_sk
self.min_gwbstart = reader.min_gwbstart
self.max_gwbstart = reader.max_gwbstart
self.min_gwbstop = reader.min_gwbstop
self.max_gwbstop = reader.max_gwbstop
self._ebands = ebands = reader.ks_bands
qplist_spin = self.qplist_spin
# TODO handle the case in which nkptgw < nkibz
self.qpgaps = reader.read_qpgaps()
self.qpenes = reader.read_qpenes()
self.ksgaps = reader.read_ksgaps()
@property
def sigma_kpoints(self):
"""The K-points where QP corrections have been calculated."""
return self.gwkpoints
[docs] def get_marker(self, qpattr):
"""
Return :class:`Marker` object associated to the QP attribute qpattr.
Used to prepare plots of KS bands with markers.
"""
# Each marker is a list of tuple(x, y, value)
x, y, s = [], [], []
for spin in range(self.nsppol):
for qp in self.qplist_spin[spin]:
ik = self.ebands.kpoints.index(qp.kpoint)
x.append(ik)
y.append(qp.e0)
size = getattr(qp, qpattr)
# Handle complex quantities
if np.iscomplex(size): size = size.real
s.append(size)
return Marker(*(x, y, s))
[docs] @lazy_property
def params(self):
""":class:`OrderedDict` with parameters that might be subject to convergence studies e.g ecuteps"""
od = self.get_ebands_params()
od.update(self.reader.read_params())
return od
[docs] def close(self):
"""Close the netcdf file."""
self.reader.close()
def __str__(self):
return self.to_string()
[docs] def to_string(self, verbose=0):
"""String representation with verbosity level ``verbose``."""
lines = []; app = lines.append
app(marquee("File Info", mark="="))
app(self.filestat(as_string=True))
app("")
app(self.structure.to_string(verbose=verbose, title="Structure"))
app("")
app(self.ebands.to_string(title="Kohn-Sham bands", with_structure=False))
app("")
# TODO: Finalize the implementation: add GW metadata.
app(marquee("QP direct gaps", mark="="))
for kgw in self.gwkpoints:
for spin in range(self.nsppol):
qp_dirgap = self.get_qpgap(spin, kgw)
app("QP_dirgap: %.3f (eV) for K-point: %s, spin: %s" % (qp_dirgap, repr(kgw), spin))
#ks_dirgap =
app("")
# Show QP results
strio = StringIO()
self.print_qps(precision=3, ignore_imag=verbose == 0, file=strio)
strio.seek(0)
app("")
app(marquee("QP results for each k-point and spin (all in eV)", mark="="))
app("".join(strio))
app("")
# TODO: Fix header.
#if verbose > 1:
# app("")
# app(marquee("Abinit Header", mark="="))
# app(self.hdr.to_string(verbose=verbose))
return "\n".join(lines)
@property
def structure(self):
"""|Structure| object."""
return self._structure
@property
def ebands(self):
"""|ElectronBands| with the KS energies."""
return self._ebands
@property
def has_spectral_function(self):
"""True if file contains spectral function data."""
return self.reader.has_spfunc
[docs] @lazy_property
def qplist_spin(self):
"""Tuple of :class:`QPList` objects indexed by spin."""
return self.reader.read_allqps()
[docs] def get_qplist(self, spin, kpoint, ignore_imag=False):
"""Return :class`QPList` for the given (spin, kpoint)"""
return self.reader.read_qplist_sk(spin, kpoint, ignore_imag=ignore_imag)
[docs] def get_qpcorr(self, spin, kpoint, band, ignore_imag=False):
"""Returns the :class:`QPState` object for the given (s, k, b)"""
return self.reader.read_qp(spin, kpoint, band, ignore_imag=ignore_imag)
[docs] @lazy_property
def qpgaps(self):
"""|numpy-array| of shape [nsppol, nkibz] with the QP direct gaps in eV."""
return self.reader.read_qpgaps()
[docs] def get_qpgap(self, spin, kpoint, with_ksgap=False):
"""Return the QP gap in eV at the given (spin, kpoint)"""
k = self.reader.kpt2fileindex(kpoint)
if not with_ksgap:
return self.qpgaps[spin, k]
else:
return self.qpgaps[spin, k], self.ksgaps[spin, k]
[docs] def read_sigee_skb(self, spin, kpoint, band):
""""
Read self-energy(w) for (spin, kpoint, band)
Return: :class:`SelfEnergy` object
"""
ik = self.reader.kpt2fileindex(kpoint)
kpoint = self.ibz[ik]
wmesh, sigxc_values = self.reader.read_sigmaw(spin, ik, band)
wmesh, spf_values = self.reader.read_spfunc(spin, ik, band)
return SelfEnergy(spin, kpoint, band, wmesh, sigxc_values, spf_values)
[docs] def print_qps(self, precision=3, ignore_imag=True, file=sys.stdout):
"""
Print QP results to stream ``file``.
Args:
precision: Number of significant digits.
ignore_imag: True if imaginary part should be ignored.
file: Output stream.
"""
from abipy.tools.printing import print_dataframe
keys = "band e0 qpe qpe_diago vxcme sigxme sigcmee0 vUme ze0".split()
for gwkpoint in self.gwkpoints:
for spin in range(self.nsppol):
df_sk = self.get_dataframe_sk(spin, gwkpoint, ignore_imag=ignore_imag)[keys]
print_dataframe(df_sk, title="K-point: %s, spin: %s" % (repr(gwkpoint), spin),
precision=precision, file=file)
[docs] def get_points_from_ebands(self, ebands_kpath, dist_tol=1e-12, size=24, verbose=0):
"""
Generate object storing the QP energies lying on the k-path used by ebands_kpath.
Mainly used to plot the QP energies in ebands.plot when the QP energies are interpolated with the SKW method.
Args:
ebands_kpath: |ElectronBands| object with the QP energies along an arbitrary k-path.
dist_tol: A point is considered to be on the path if its distance from the line
is less than dist_tol.
size: The marker size in points**2
verbose: Verbosity level
Return:
Example::
r = sigres.interpolate(lpratio=5,
ks_ebands_kpath=ks_ebands_kpath,
ks_ebands_kmesh=ks_ebands_kmesh
)
points = sigres.get_points_from_ebands(r.qp_ebands_kpath, size=24)
r.qp_ebands_kpath.plot(points=points)
"""
kpath = ebands_kpath.kpoints
if not ebands_kpath.kpoints.is_path:
raise TypeError("Expecting band structure with a Kpath, got %s" % type(kpath))
if verbose:
print("Input kpath\n", ebands_kpath.kpoints)
print("gwkpoints included in GW calculation\n", self.gwkpoints)
print("lines\n", kpath.lines)
print("kpath.frac_bounds:\n", kpath.frac_bounds)
print("kpath.cart_bounds:\n", kpath.frac_bounds)
# Construct the stars of the k-points for all k-points included in the GW calculation.
# In principle, the input k-path is arbitrary and not necessarily in the IBZ used for GW
# so we have to build the k-stars and find the k-points lying along the path and keep
# track of the mapping kpt --> star --> kgw
gw_stars = [kpoint.compute_star(self.structure.abi_spacegroup.fm_symmops) for kpoint in self.gwkpoints]
cart_coords, back2istar = [], []
for istar, gw_star in enumerate(gw_stars):
cart_coords.extend([k.cart_coords for k in gw_star])
back2istar.extend([istar] * len(gw_star))
cart_coords = np.reshape(cart_coords, (-1, 3))
# Find (star) k-points on the path.
p = kpath.find_points_along_path(cart_coords, dist_tol=dist_tol)
if len(p.ikfound) == 0:
cprint("Warning: Found zero points lying on the input k-path. Try to increase dist_tol.", "yellow")
return Marker()
# Read complex GW energies from file.
qp_arr = self.reader.read_value("egw", cmode="c")
# Each marker is a list of tuple(x, y, value)
x, y, s = [], [], []
kpath_lenght = kpath.ds.sum()
for ik, dalong_path in zip(p.ikfound, p.dist_list):
istar = back2istar[ik]
# The k-point used in the GW calculation.
gwk = gw_stars[istar].base_point
# Indices needed to access SIGRES arrays (we have to live with this format)
ik_ibz = self.reader.kpt2fileindex(gwk)
ik_b = self.reader.gwkpt2seqindex(gwk)
for spin in range(self.nsppol):
# Need to select bands included in the GW calculation.
for qpe in qp_arr[spin, ik_ibz, self.gwbstart_sk[spin, ik_b]:self.gwbstop_sk[spin, ik_b]]:
# Assume the path is properly normalized.
x.append((dalong_path / kpath_lenght) * (len(kpath) - 1))
y.append(qpe.real)
s.append(size)
return Marker(*(x, y, s))
[docs] @add_fig_kwargs
def plot_qpgaps(self, ax=None, plot_qpmks=True, fontsize=8, **kwargs):
"""
Plot the KS and the QP direct gaps for all the k-points and spins available on file.
Args:
ax: |matplotlib-Axes| or None if a new figure should be created.
plot_qpmks: If False, plot QP_gap, KS_gap else (QP_gap - KS_gap)
fontsize: legend and title fontsize.
kwargs: Passed to ax.plot method except for marker.
Returns: |matplotlib-Figure|
"""
ax, fig, plt = get_ax_fig_plt(ax=ax)
label = kwargs.pop("label", None)
xs = np.arange(self.nkcalc)
# Add xticklabels from k-points.
tick_labels = [repr(k) for k in self.gwkpoints]
ax.set_xticks(xs)
ax.set_xticklabels(tick_labels, fontdict=None, rotation=30, minor=False, size="x-small")
for spin in range(self.nsppol):
qp_gaps, ks_gaps = map(np.array, zip(*[self.get_qpgap(spin, kgw, with_ksgap=True) for kgw in self.gwkpoints]))
if not plot_qpmks:
# Plot QP gaps
ax.plot(xs, qp_gaps, marker=self.marker_spin[spin],
label=label if spin == 0 else None, **kwargs)
# Add KS gaps
#ax.scatter(xx, ks_gaps) #, label="KS gap %s" % label)
else:
# Plot QP_gap - KS_gap
ax.plot(xs, qp_gaps - ks_gaps, marker=self.marker_spin[spin],
label=label if spin == 0 else None, **kwargs)
ax.grid(True)
ax.set_xlabel("K-point")
if plot_qpmks:
ax.set_ylabel("QP-KS gap (eV)")
else:
ax.set_ylabel("QP direct gap (eV)")
#ax.set_title("k:%s" % (repr(kgw)), fontsize=fontsize)
if label:
ax.legend(loc="best", fontsize=fontsize, shadow=True)
return fig
[docs] @add_fig_kwargs
def plot_qps_vs_e0(self, with_fields="all", exclude_fields=None, e0="fermie",
xlims=None, sharey=False, ax_list=None, fontsize=8, **kwargs):
"""
Plot QP result in SIGRES file as function of the KS energy.
Args:
with_fields: The names of the qp attributes to plot as function of eKS.
Accepts: List of strings or string with tokens separated by blanks.
See :class:`QPState` for the list of available fields.
exclude_fields: Similar to ``with_fields`` but excludes fields
e0: Option used to define the zero of energy in the band structure plot. Possible values:
- `fermie`: shift all eigenvalues to have zero energy at the Fermi energy (`self.fermie`).
- Number e.g e0=0.5: shift all eigenvalues to have zero energy at 0.5 eV
- None: Don't shift energies, equivalent to e0=0
ax_list: List of |matplotlib-Axes| for plot. If None, new figure is produced.
xlims: Set the data limits for the x-axis. Accept tuple e.g. ``(left, right)``
or scalar e.g. ``left``. If left (right) is None, default values are used.
sharey: True if y-axis should be shared.
fontsize: Legend and title fontsize.
Returns: |matplotlib-Figure|
"""
with_fields = QPState.get_fields_for_plot(with_fields, exclude_fields)
# Because qplist does not have the fermi level.
fermie = self.ebands.get_e0(e0) if e0 is not None else None
for spin in range(self.nsppol):
fig = self.qplist_spin[spin].plot_qps_vs_e0(
with_fields=with_fields, exclude_fields=exclude_fields, fermie=fermie,
xlims=xlims, sharey=sharey, ax_list=ax_list, fontsize=fontsize,
marker=self.marker_spin[spin], show=False, **kwargs)
ax_list = fig.axes
return fig
[docs] @add_fig_kwargs
def plot_spectral_functions(self, include_bands=None, fontsize=8, **kwargs):
"""
Plot the spectral function for all k-points, bands and spins available in the SIGRES file.
Args:
include_bands: List of bands to include. Nonee means all.
fontsize: Legend and title fontsize.
Returns: |matplotlib-Figure|
"""
if include_bands is not None:
include_bands = set(include_bands)
# Build grid of plots.
nrows, ncols = len(self.sigma_kpoints), 1
ax_list = None
ax_list, fig, plt = get_axarray_fig_plt(ax_list, nrows=nrows, ncols=ncols,
sharex=True, sharey=False, squeeze=False)
ax_list = np.array(ax_list).ravel()
for ik_gw, (kgw, ax) in enumerate(zip(self.sigma_kpoints, ax_list)):
for spin in range(self.nsppol):
for band in range(self.gwbstart_sk[spin, ik_gw], self.gwbstop_sk[spin, ik_gw]):
if include_bands and band not in include_bands: continue
sigw = self.read_sigee_skb(spin, kgw, band)
label = r"$A(\omega)$: band: %d, spin: %d" % (spin, band)
sigw.plot_ax(ax, what="a", label=label, fontsize=fontsize, **kwargs)
ax.set_title("K-point: %s" % repr(sigw.kpoint), fontsize=fontsize)
#if ik_gw == len(self.sigma_kpoints) - 1:
return fig
[docs] @add_fig_kwargs
def plot_eigvec_qp(self, spin=0, kpoint=None, band=None, **kwargs):
"""
Args:
spin: Spin index
kpoint: K-point in self-energy. Accepts |Kpoint|, vector or index.
If None, all k-points for the given ``spin`` are shown.
band: band index. If None all bands are displayed else
only <KS_b|QP_{b'}> for the given b.
kwargs: Passed to plot method of :class:`ArrayPlotter`.
Returns: |matplotlib-Figure|
"""
plotter = ArrayPlotter()
if kpoint is None:
for kpoint in self.ibz:
ksqp_arr = self.reader.read_eigvec_qp(spin, kpoint, band=band)
plotter.add_array(repr(kpoint), ksqp_arr)
return plotter.plot(show=False, **kwargs)
else:
ksqp_arr = self.reader.read_eigvec_qp(spin, kpoint, band=band)
plotter.add_array(repr(kpoint), ksqp_arr)
return plotter.plot(show=False, **kwargs)
[docs] @add_fig_kwargs
def plot_qpbands_ibz(self, e0="fermie", colormap="jet", ylims=None, fontsize=8, **kwargs):
r"""
Plot the KS band structure in the IBZ with the QP energies.
Args:
e0: Option used to define the zero of energy in the band structure plot.
colormap: matplotlib color map.
ylims: Set the data limits for the y-axis. Accept tuple e.g. ``(left, right)``
or scalar e.g. ``left``. If left (right) is None, default values are used.
fontsize: Legend and title fontsize.
Returns: |matplotlib-Figure|
"""
# Map sigma_kpoints to ebands.kpoints
kcalc2ibz = np.empty(self.nkcalc, dtype=int)
for ikc, sigkpt in enumerate(self.sigma_kpoints):
kcalc2ibz[ikc] = self.ebands.kpoints.index(sigkpt)
# TODO: It seems there's a minor issue with fermie if SCF band structure.
e0 = self.ebands.get_e0(e0)
#print("e0",e0, self.ebands.fermie)
# Build grid with (1, nsppol) plots.
nrows, ncols = 1, self.nsppol
ax_list, fig, plt = get_axarray_fig_plt(None, nrows=nrows, ncols=ncols,
sharex=True, sharey=True, squeeze=False)
ax_list = np.array(ax_list).ravel()
cmap = plt.get_cmap(colormap)
# Read QP energies: Fortran egw(nbnds,nkibz,nsppol)
qpes = self.reader.read_value("egw", cmode="c") # * units.Ha_to_eV
band_range = (self.reader.max_gwbstart, self.reader.min_gwbstop)
nb = self.reader.min_gwbstop - self.reader.min_gwbstart
for spin, ax in zip(range(self.nsppol), ax_list):
# Plot KS bands in the band range included in self-energy calculation.
self.ebands.plot(ax=ax, e0=e0, spin=spin, band_range=band_range, show=False)
# Extract QP in IBZ
yvals = qpes[spin, kcalc2ibz, :].real - e0
# Add (scattered) QP energies for the calculated k-points.
for band in range(self.reader.max_gwbstart, self.reader.min_gwbstop):
ax.scatter(kcalc2ibz, yvals[:, band],
color=cmap(band / nb), alpha=0.6, marker="o", s=20,
)
set_axlims(ax, ylims, "y")
return fig
[docs] @add_fig_kwargs
def plot_ksbands_with_qpmarkers(self, qpattr="qpeme0", e0="fermie", fact=1000, ax=None, **kwargs):
"""
Plot the KS energies as function of k-points and add markers whose size
is proportional to the QPState attribute ``qpattr``
Args:
qpattr: Name of the QP attribute to plot. See :class:`QPState`.
e0: Option used to define the zero of energy in the band structure plot. Possible values:
- ``fermie``: shift all eigenvalues to have zero energy at the Fermi energy (``self.fermie``).
- Number e.g ``e0 = 0.5``: shift all eigenvalues to have zero energy at 0.5 eV
- None: Don't shift energies, equivalent to ``e0 = 0``
fact: Markers are multiplied by this factor.
ax: |matplotlib-Axes| or None if a new figure should be created.
Returns: |matplotlib-Figure|
"""
ax, fig, plt = get_ax_fig_plt(ax=ax)
gwband_range = self.min_gwbstart, self.max_gwbstop
self.ebands.plot(band_range=gwband_range, e0=e0, ax=ax, show=False, **kwargs)
e0 = self.ebands.get_e0(e0)
marker = self.get_marker(qpattr)
pos, neg = marker.posneg_marker()
# Use different symbols depending on the value of s. Cannot use negative s.
if pos:
ax.scatter(pos.x, pos.y - e0, s=np.abs(pos.s) * fact, marker="^", label=qpattr + " >0")
if neg:
ax.scatter(neg.x, neg.y - e0, s=np.abs(neg.s) * fact, marker="v", label=qpattr + " <0")
return fig
[docs] def get_dataframe(self, ignore_imag=False):
"""
Returns |pandas-DataFrame| with QP results for all k-points included in the GW calculation
Args:
ignore_imag: Only real part is returned if ``ignore_imag``.
"""
df_list = []
for spin in range(self.nsppol):
for gwkpoint in self.gwkpoints:
df_sk = self.get_dataframe_sk(spin, gwkpoint, ignore_imag=ignore_imag)
df_list.append(df_sk)
return pd.concat(df_list)
# FIXME: To maintain previous interface.
to_dataframe = get_dataframe
[docs] def get_dataframe_sk(self, spin, kpoint, index=None, ignore_imag=False, with_params=True):
"""
Returns |pandas-DataFrame| with QP results for the given (spin, k-point).
Args:
ignore_imag: Only real part is returned if ``ignore_imag``.
with_params: True to include convergence paramenters.
"""
rows, bands = [], []
# bstart and bstop depends on kpoint.
ik_gw = self.reader.gwkpt2seqindex(kpoint)
for band in range(self.gwbstart_sk[spin, ik_gw], self.gwbstop_sk[spin, ik_gw]):
bands.append(band)
# Build dictionary with the QP results.
qpstate = self.reader.read_qp(spin, kpoint, band, ignore_imag=ignore_imag)
d = qpstate.as_dict()
# Add other entries that may be useful when comparing different calculations.
if with_params:
d.update(self.params)
rows.append(d)
import pandas as pd
index = len(bands) * [index] if index is not None else bands
return pd.DataFrame(rows, index=index, columns=list(rows[0].keys()))
#def plot_matrix_elements(self, mel_name, spin, kpoint, *args, **kwargs):
# matrix = self.reader.read_mel(mel_name, spin, kpoint):
# return plot_matrix(matrix, *args, **kwargs)
#def plot_mlda_to_qps(self, spin, kpoint, *args, **kwargs):
# matrix = self.reader.read_mlda_to_qps(spin, kpoint)
# return plot_matrix(matrix, *args, **kwargs)
[docs] def interpolate(self, lpratio=5, ks_ebands_kpath=None, ks_ebands_kmesh=None, ks_degatol=1e-4,
vertices_names=None, line_density=20, filter_params=None, only_corrections=False, verbose=0):
"""
Interpolate the GW corrections in k-space on a k-path and, optionally, on a k-mesh.
Args:
lpratio: Ratio between the number of star functions and the number of ab-initio k-points.
The default should be OK in many systems, larger values may be required for accurate derivatives.
ks_ebands_kpath: KS |ElectronBands| on a k-path. If present,
the routine interpolates the QP corrections and apply them on top of the KS band structure
This is the recommended option because QP corrections are usually smoother than the
QP energies and therefore easier to interpolate. If None, the QP energies are interpolated
along the path defined by ``vertices_names`` and ``line_density``.
ks_ebands_kmesh: KS |ElectronBands| on a homogeneous k-mesh. If present, the routine
interpolates the corrections on the k-mesh (used to compute QP the DOS)
ks_degatol: Energy tolerance in eV. Used when either ``ks_ebands_kpath`` or ``ks_ebands_kmesh`` are given.
KS energies are assumed to be degenerate if they differ by less than this value.
The interpolator may break band degeneracies (the error is usually smaller if QP corrections
are interpolated instead of QP energies). This problem can be partly solved by averaging
the interpolated values over the set of KS degenerate states.
A negative value disables this ad-hoc symmetrization.
vertices_names: Used to specify the k-path for the interpolated QP band structure
when ``ks_ebands_kpath`` is None.
It's a list of tuple, each tuple is of the form (kfrac_coords, kname) where
kfrac_coords are the reduced coordinates of the k-point and kname is a string with the name of
the k-point. Each point represents a vertex of the k-path. ``line_density`` defines
the density of the sampling. If None, the k-path is automatically generated according
to the point group of the system.
line_density: Number of points in the smallest segment of the k-path. Used with ``vertices_names``.
filter_params: TO BE DESCRIBED
only_corrections: If True, the output contains the interpolated QP corrections instead of the QP energies.
Available only if ks_ebands_kpath and/or ks_ebands_kmesh are used.
verbose: Verbosity level
Returns:
:class:`namedtuple` with the following attributes::
* qp_ebands_kpath: |ElectronBands| with the QP energies interpolated along the k-path.
* qp_ebands_kmesh: |ElectronBands| with the QP energies interpolated on the k-mesh.
None if ``ks_ebands_kmesh`` is not passed.
* ks_ebands_kpath: |ElectronBands| with the KS energies interpolated along the k-path.
* ks_ebands_kmesh: |ElectronBands| with the KS energies on the k-mesh..
None if ``ks_ebands_kmesh`` is not passed.
* interpolator: |SkwInterpolator| object.
"""
# TODO: Consistency check.
errlines = []
eapp = errlines.append
if len(self.gwkpoints) != len(self.ibz):
eapp("QP energies should be computed for all k-points in the IBZ but nkibz != nkptgw")
if len(self.gwkpoints) == 1:
eapp("QP Interpolation requires nkptgw > 1.")
#if (np.any(self.gwbstop_sk[0, 0] != self.gwbstop_sk):
# cprint("Highest bdgw band is not constant over k-points. QP Bands will be interpolated up to...")
#if (np.any(self.gwbstart_sk[0, 0] != self.gwbstart_sk):
#if (np.any(self.gwbstart_sk[0, 0] != 0):
if errlines:
raise ValueError("\n".join(errlines))
# Get symmetries from abinit spacegroup (read from file).
abispg = self.structure.abi_spacegroup
fm_symrel = [s for (s, afm) in zip(abispg.symrel, abispg.symafm) if afm == 1]
if ks_ebands_kpath is None:
# Generate k-points for interpolation. Will interpolate all bands available in the sigres file.
bstart, bstop = 0, -1
if vertices_names is None:
vertices_names = [(k.frac_coords, k.name) for k in self.structure.hsym_kpoints]
kpath = Kpath.from_vertices_and_names(self.structure, vertices_names, line_density=line_density)
kfrac_coords, knames = kpath.frac_coords, kpath.names
else:
# Use list of k-points from ks_ebands_kpath.
ks_ebands_kpath = ElectronBands.as_ebands(ks_ebands_kpath)
kfrac_coords = [k.frac_coords for k in ks_ebands_kpath.kpoints]
knames = [k.name for k in ks_ebands_kpath.kpoints]
# Find the band range for the interpolation.
bstart, bstop = 0, ks_ebands_kpath.nband
bstop = min(bstop, self.min_gwbstop)
if ks_ebands_kpath.nband < self.min_gwbstop:
cprint("Number of bands in KS band structure smaller than the number of bands in GW corrections", "red")
cprint("Highest GW bands will be ignored", "red")
if not ks_ebands_kpath.kpoints.is_path:
cprint("Energies in ks_ebands_kpath should be along a k-path!", "red")
# Interpolate QP energies if ks_ebands_kpath is None else interpolate QP corrections
# and re-apply them on top of the KS band structure.
gw_kcoords = [k.frac_coords for k in self.gwkpoints]
# Read GW energies from file (real part) and compute corrections if ks_ebands_kpath.
egw_rarr = self.reader.read_value("egw", cmode="c").real
if ks_ebands_kpath is not None:
if ks_ebands_kpath.structure != self.structure:
cprint("sigres.structure and ks_ebands_kpath.structures differ. Check your files!", "red")
egw_rarr -= self.reader.read_value("e0")
# Note there's no guarantee that the gwkpoints and the corrections have the same k-point index.
# Be careful because the order of the k-points and the band range stored in the SIGRES file may differ ...
qpdata = np.empty(egw_rarr.shape)
for gwk in self.gwkpoints:
ik_ibz = self.reader.kpt2fileindex(gwk)
for spin in range(self.nsppol):
qpdata[spin, ik_ibz, :] = egw_rarr[spin, ik_ibz, :]
# Build interpolator for QP corrections.
from abipy.core.skw import SkwInterpolator
cell = (self.structure.lattice.matrix, self.structure.frac_coords, self.structure.atomic_numbers)
qpdata = qpdata[:, :, bstart:bstop]
# Old sigres files do not have kptopt.
has_timrev = has_timrev_from_kptopt(self.reader.read_value("kptopt", default=1))
skw = SkwInterpolator(lpratio, gw_kcoords, qpdata, self.ebands.fermie, self.ebands.nelect,
cell, fm_symrel, has_timrev,
filter_params=filter_params, verbose=verbose)
if ks_ebands_kpath is None:
# Interpolate QP energies.
eigens_kpath = skw.interp_kpts(kfrac_coords).eigens
else:
# Interpolate QP energies corrections and add them to KS.
ref_eigens = ks_ebands_kpath.eigens[:, :, bstart:bstop]
qp_corrs = skw.interp_kpts_and_enforce_degs(kfrac_coords, ref_eigens, atol=ks_degatol).eigens
eigens_kpath = qp_corrs if only_corrections else ref_eigens + qp_corrs
# Build new ebands object with k-path.
kpts_kpath = Kpath(self.structure.reciprocal_lattice, kfrac_coords, weights=None, names=knames)
occfacts_kpath = np.zeros(eigens_kpath.shape)
# Finding the new Fermi level of the interpolated bands is not trivial, in particular if metallic.
# because one should first interpolate the QP bands on a mesh. Here I align the QP bands
# at the HOMO of the KS bands.
homos = ks_ebands_kpath.homos if ks_ebands_kpath is not None else self.ebands.homos
qp_fermie = max([eigens_kpath[e.spin, e.kidx, e.band] for e in homos])
#qp_fermie = self.ebands.fermie
#qp_fermie = 0.0
qp_ebands_kpath = ElectronBands(self.structure, kpts_kpath, eigens_kpath, qp_fermie, occfacts_kpath,
self.ebands.nelect, self.ebands.nspinor, self.ebands.nspden,
smearing=self.ebands.smearing)
qp_ebands_kmesh = None
if ks_ebands_kmesh is not None:
# Interpolate QP corrections on the same k-mesh as the one used in the KS run.
ks_ebands_kmesh = ElectronBands.as_ebands(ks_ebands_kmesh)
if bstop > ks_ebands_kmesh.nband:
raise ValueError("Not enough bands in ks_ebands_kmesh, found %s, minimum expected %d\n" % (
ks_ebands_kmesh.nband, bstop))
if ks_ebands_kpath.structure != self.structure:
cprint("sigres.structure and ks_ebands_kpath.structures differ. Check your files!", "red")
#raise ValueError("sigres.structure and ks_ebands_kmesh.structures differ. Check your files!")
if not ks_ebands_kmesh.kpoints.is_ibz:
cprint("Energies in ks_ebands_kmesh should be given in the IBZ", "red")
# K-points and weight for DOS are taken from ks_ebands_kmesh
dos_kcoords = [k.frac_coords for k in ks_ebands_kmesh.kpoints]
dos_weights = [k.weight for k in ks_ebands_kmesh.kpoints]
# Interpolate QP corrections from bstart to bstop
ref_eigens = ks_ebands_kmesh.eigens[:, :, bstart:bstop]
qp_corrs = skw.interp_kpts_and_enforce_degs(dos_kcoords, ref_eigens, atol=ks_degatol).eigens
eigens_kmesh = qp_corrs if only_corrections else ref_eigens + qp_corrs
# Build new ebands object with k-mesh
kpts_kmesh = IrredZone(self.structure.reciprocal_lattice, dos_kcoords, weights=dos_weights,
names=None, ksampling=ks_ebands_kmesh.kpoints.ksampling)
occfacts_kmesh = np.zeros(eigens_kmesh.shape)
qp_ebands_kmesh = ElectronBands(self.structure, kpts_kmesh, eigens_kmesh, qp_fermie, occfacts_kmesh,
self.ebands.nelect, self.ebands.nspinor, self.ebands.nspden,
smearing=self.ebands.smearing)
return dict2namedtuple(qp_ebands_kpath=qp_ebands_kpath,
qp_ebands_kmesh=qp_ebands_kmesh,
ks_ebands_kpath=ks_ebands_kpath,
ks_ebands_kmesh=ks_ebands_kmesh,
interpolator=skw,
)
[docs] def yield_figs(self, **kwargs): # pragma: no cover
"""
This function *generates* a predefined list of matplotlib figures with minimal input from the user.
Used in abiview.py to get a quick look at the results.
"""
yield self.plot_qpgaps(show=False)
yield self.plot_qps_vs_e0(show=False)
yield self.plot_qpbands_ibz(show=False)
yield self.plot_ksbands_with_qpmarkers(show=False)
if self.has_spectral_function:
yield self.plot_spectral_functions(include_bands=None, show=False)
[docs] def write_notebook(self, nbpath=None):
"""
Write a jupyter_ notebook to ``nbpath``. If nbpath is None, a temporay file in the current
working directory is created. Return path to the notebook.
"""
nbformat, nbv, nb = self.get_nbformat_nbv_nb(title=None)
nb.cells.extend([
nbv.new_code_cell("sigres = abilab.abiopen('%s')" % self.filepath),
nbv.new_code_cell("print(sigres)"),
nbv.new_code_cell("sigres.plot_qps_vs_e0();"),
nbv.new_code_cell("sigres.plot_spectral_functions(spin=0, kpoint=[0, 0, 0], bands=0);"),
nbv.new_code_cell("#sigres.plot_ksbands_with_qpmarkers(qpattr='qpeme0', fact=100);"),
nbv.new_code_cell("r = sigres.interpolate(ks_ebands_kpath=None, ks_ebands_kmesh=None); print(r.interpolator)"),
nbv.new_code_cell("r.qp_ebands_kpath.plot();"),
nbv.new_code_cell("""
if r.ks_ebands_kpath is not None:
plotter = abilab.ElectronBandsPlotter()
plotter.add_ebands("KS", r.ks_ebands_kpath) # dos=r.ks_ebands_kmesh.get_edos())
plotter.add_ebands("GW (interpolated)", r.qp_ebands_kpath) # dos=r.qp_ebands_kmesh.get_edos()))
plotter.ipw_select_plot()"""),
])
return self._write_nb_nbpath(nb, nbpath)
class SigresReader(ETSF_Reader):
r"""
This object provides method to read data from the SIGRES file produced ABINIT.
.. rubric:: Inheritance Diagram
.. inheritance-diagram:: SigresReader
# See 70gw/m_sigma_results.F90
# Name of the diagonal matrix elements stored in the file.
# b1gw:b2gw,nkibz,nsppol*nsig_ab))
#_DIAGO_MELS = [
# "sigxme",
# "vxcme",
# "vUme",
# "dsigmee0",
# "sigcmee0",
# "sigxcme",
# "ze0",
#]
integer :: b1gw,b2gw ! min and Max gw band indeces over spin and k-points (used to dimension)
integer :: gwcalctyp ! Flag defining the calculation type.
integer :: nkptgw ! No. of points calculated
integer :: nkibz ! No. of irreducible k-points.
integer :: nbnds ! Total number of bands
integer :: nomega_r ! No. of real frequencies for the spectral function.
integer :: nomega_i ! No. of frequencies along the imaginary axis.
integer :: nomega4sd ! No. of real frequencies to evaluate the derivative of $\Sigma(E)$.
integer :: nsig_ab ! 1 if nspinor=1,4 for noncollinear case.
integer :: nsppol ! No. of spin polarizations.
integer :: usepawu ! 1 if we are using LDA+U as starting point (only for PAW)
real(dp) :: deltae ! Frequency step for the calculation of d\Sigma/dE
real(dp) :: maxomega4sd ! Max frequency around E_ks for d\Sigma/dE.
real(dp) :: maxomega_r ! Max frequency for spectral function.
real(dp) :: scissor_ene ! Scissor energy value. zero for None.
integer,pointer :: maxbnd(:,:)
! maxbnd(nkptgw,nsppol)
! Max band index considered in GW for this k-point.
integer,pointer :: minbnd(:,:)
! minbnd(nkptgw,nsppol)
! Min band index considered in GW for this k-point.
real(dp),pointer :: degwgap(:,:)
! degwgap(nkibz,nsppol)
! Difference btw the QPState and the KS optical gap.
real(dp),pointer :: egwgap(:,:)
! egwgap(nkibz,nsppol))
! QPState optical gap at each k-point and spin.
real(dp),pointer :: en_qp_diago(:,:,:)
! en_qp_diago(nbnds,nkibz,nsppol))
! QPState energies obtained from the diagonalization of the Hermitian approximation to Sigma (QPSCGW)
real(dp),pointer :: e0(:,:,:)
! e0(nbnds,nkibz,nsppol)
! KS eigenvalues for each band, k-point and spin. In case of self-consistent?
real(dp),pointer :: e0gap(:,:)
! e0gap(nkibz,nsppol),
! KS gap at each k-point, for each spin.
real(dp),pointer :: omega_r(:)
! omega_r(nomega_r)
! real frequencies used for the self energy.
real(dp),pointer :: kptgw(:,:)
! kptgw(3,nkptgw)
! ! TODO there is a similar array in sigma_parameters
! List of calculated k-points.
real(dp),pointer :: sigxme(:,:,:)
! sigxme(b1gw:b2gw,nkibz,nsppol*nsig_ab))
! Diagonal matrix elements of $\Sigma_x$ i.e $\<nks|\Sigma_x|nks\>$
real(dp),pointer :: vxcme(:,:,:)
! vxcme(b1gw:b2gw,nkibz,nsppol*nsig_ab))
! $\<nks|v_{xc}[n_val]|nks\>$ matrix elements of vxc (valence-only contribution).
real(dp),pointer :: vUme(:,:,:)
! vUme(b1gw:b2gw,nkibz,nsppol*nsig_ab))
! $\<nks|v_{U}|nks\>$ for LDA+U.
complex(dpc),pointer :: degw(:,:,:)
! degw(b1gw:b2gw,nkibz,nsppol))
! Difference between the QPState and the KS energies.
complex(dpc),pointer :: dsigmee0(:,:,:)
! dsigmee0(b1gw:b2gw,nkibz,nsppol*nsig_ab))
! Derivative of $\Sigma_c(E)$ calculated at the KS eigenvalue.
complex(dpc),pointer :: egw(:,:,:)
! egw(nbnds,nkibz,nsppol))
! QPState energies, $\epsilon_{nks}^{QPState}$.
complex(dpc),pointer :: eigvec_qp(:,:,:,:)
! eigvec_qp(nbnds,nbnds,nkibz,nsppol))
! Expansion of the QPState amplitude in the KS basis set.
complex(dpc),pointer :: hhartree(:,:,:,:)
! hhartree(b1gw:b2gw,b1gw:b2gw,nkibz,nsppol*nsig_ab)
! $\<nks|T+v_H+v_{loc}+v_{nl}|mks\>$
complex(dpc),pointer :: sigcme(:,:,:,:)
! sigcme(b1gw:b2gw,nkibz,nomega_r,nsppol*nsig_ab))
! $\<nks|\Sigma_{c}(E)|nks\>$ at each nomega_r frequency
complex(dpc),pointer :: sigmee(:,:,:)
! sigmee(b1gw:b2gw,nkibz,nsppol*nsig_ab))
! $\Sigma_{xc}E_{KS} + (E_{QPState}- E_{KS})*dSigma/dE_KS
complex(dpc),pointer :: sigcmee0(:,:,:)
! sigcmee0(b1gw:b2gw,nkibz,nsppol*nsig_ab))
! Diagonal mat. elements of $\Sigma_c(E)$ calculated at the KS energy $E_{KS}$
complex(dpc),pointer :: sigcmesi(:,:,:,:)
! sigcmesi(b1gw:b2gw,nkibz,nomega_i,nsppol*nsig_ab))
! Matrix elements of $\Sigma_c$ along the imaginary axis.
! Only used in case of analytical continuation.
complex(dpc),pointer :: sigcme4sd(:,:,:,:)
! sigcme4sd(b1gw:b2gw,nkibz,nomega4sd,nsppol*nsig_ab))
! Diagonal matrix elements of \Sigma_c around the zeroth order eigenvalue (usually KS).
complex(dpc),pointer :: sigxcme(:,:,:,:)
! sigxme(b1gw:b2gw,nkibz,nomega_r,nsppol*nsig_ab))
! $\<nks|\Sigma_{xc}(E)|nks\>$ at each real frequency frequency.
complex(dpc),pointer :: sigxcmesi(:,:,:,:)
! sigxcmesi(b1gw:b2gw,nkibz,nomega_i,nsppol*nsig_ab))
! Matrix elements of $\Sigma_{xc}$ along the imaginary axis.
! Only used in case of analytical continuation.
complex(dpc),pointer :: sigxcme4sd(:,:,:,:)
! sigxcme4sd(b1gw:b2gw,nkibz,nomega4sd,nsppol*nsig_ab))
! Diagonal matrix elements of \Sigma_xc for frequencies around the zeroth order eigenvalues.
complex(dpc),pointer :: ze0(:,:,:)
! ze0(b1gw:b2gw,nkibz,nsppol))
! renormalization factor. $(1-\dfrac{\partial\Sigma_c} {\partial E_{KS}})^{-1}$
complex(dpc),pointer :: omega_i(:)
! omegasi(nomega_i)
! Frequencies along the imaginary axis used for the analytical continuation.
complex(dpc),pointer :: omega4sd(:,:,:,:)
! omega4sd(b1gw:b2gw,nkibz,nomega4sd,nsppol).
! Frequencies used to evaluate the Derivative of Sigma.
"""
def __init__(self, path):
self.ks_bands = ElectronBands.from_file(path)
self.nsppol = self.ks_bands.nsppol
super().__init__(path)
try:
self.nomega_r = self.read_dimvalue("nomega_r")
except self.Error:
self.nomega_r = 0
#self.nomega_i = self.read_dim("nomega_i")
# Save important quantities needed to simplify the API.
self.structure = self.read_structure()
self.gwcalctyp = self.read_value("gwcalctyp")
self.usepawu = self.read_value("usepawu")
# 1) The K-points of the homogeneous mesh.
self.ibz = self.ks_bands.kpoints
# 2) The K-points where QPState corrections have been calculated.
gwred_coords = self.read_redc_gwkpoints()
self.gwkpoints = KpointList(self.structure.reciprocal_lattice, gwred_coords)
# Find k-point name
for kpoint in self.gwkpoints:
kpoint.set_name(self.structure.findname_in_hsym_stars(kpoint))
# minbnd[nkptgw,nsppol] gives the minimum band index computed
# Note conversion between Fortran and python convention.
self.gwbstart_sk = self.read_value("minbnd") - 1
self.gwbstop_sk = self.read_value("maxbnd")
# min and Max band index for GW corrections.
self.min_gwbstart = np.min(self.gwbstart_sk)
self.max_gwbstart = np.max(self.gwbstart_sk)
self.min_gwbstop = np.min(self.gwbstop_sk)
self.max_gwbstop = np.max(self.gwbstop_sk)
self._egw = self.read_value("egw", cmode="c")
# Read and save important matrix elements.
# All these arrays are dimensioned
# vxcme(b1gw:b2gw,nkibz,nsppol*nsig_ab))
self._vxcme = self.read_value("vxcme")
self._sigxme = self.read_value("sigxme")
self._hhartree = self.read_value("hhartree", cmode="c")
self._vUme = self.read_value("vUme")
#if self.usepawu == 0: self._vUme.fill(0.0)
# Complex arrays
self._sigcmee0 = self.read_value("sigcmee0", cmode="c")
self._ze0 = self.read_value("ze0", cmode="c")
# Frequencies for the spectral function.
# Note that omega_r does not depend on (s, k, b).
if self.has_spfunc:
self._omega_r = self.read_value("omega_r")
self._sigcme = self.read_value("sigcme", cmode="c")
self._sigxcme = self.read_value("sigxcme", cmode="c")
# Self-consistent case
self._en_qp_diago = self.read_value("en_qp_diago")
#self._mlda_to_qp
#def is_selfconsistent(self, mode):
# return self.gwcalctyp
@property
def has_spfunc(self):
"""True if self contains the spectral function."""
return self.nomega_r
def kpt2fileindex(self, kpoint):
"""
Helper function that returns the index of kpoint in the netcdf file.
Accepts |Kpoint| instance or integer
Raise:
`KpointsError` if kpoint cannot be found.
.. note::
This function is needed since arrays in the netcdf file are dimensioned
with the total number of k-points in the IBZ.
"""
if duck.is_intlike(kpoint): return int(kpoint)
return self.ibz.index(kpoint)
def gwkpt2seqindex(self, gwkpoint):
"""
This function returns the index of the GW k-point in (0:nkptgw)
Used to access data in the arrays that are dimensioned [0:nkptgw] e.g. minbnd.
"""
if duck.is_intlike(gwkpoint):
return int(gwkpoint)
else:
return self.gwkpoints.index(gwkpoint)
def read_redc_gwkpoints(self):
return self.read_value("kptgw")
def read_allqps(self, ignore_imag=False):
"""
Return list with ``nsppol`` items. Each item is a :class:`QPList` with the QP results
Args:
ignore_imag: Only real part is returned if ``ignore_imag``.
"""
qps_spin = self.nsppol * [None]
for spin in range(self.nsppol):
qps = []
for gwkpoint in self.gwkpoints:
ik = self.gwkpt2seqindex(gwkpoint)
for band in range(self.gwbstart_sk[spin, ik], self.gwbstop_sk[spin, ik]):
qps.append(self.read_qp(spin, gwkpoint, band, ignore_imag=ignore_imag))
qps_spin[spin] = QPList(qps)
return tuple(qps_spin)
def read_qplist_sk(self, spin, kpoint, ignore_imag=False):
"""
Read and return :class:`QPList` object for the given spin, kpoint.
Args:
ignore_imag: Only real part is returned if ``ignore_imag``.
"""
ik = self.gwkpt2seqindex(kpoint)
bstart, bstop = self.gwbstart_sk[spin, ik], self.gwbstop_sk[spin, ik]
return QPList([self.read_qp(spin, kpoint, band, ignore_imag=ignore_imag)
for band in range(bstart, bstop)])
#def read_qpene(self, spin, kpoint, band)
def read_qpenes(self):
return self._egw[:, :, :]
def read_qp(self, spin, kpoint, band, ignore_imag=False):
"""
Return :class`QPState` for the given (spin, kpoint, band).
Only real part is returned if ``ignore_imag``.
"""
ik_file = self.kpt2fileindex(kpoint)
# Must shift band index (see fortran code that allocates with mdbgw)
ib_gw = band - self.min_gwbstart
def ri(a):
return np.real(a) if ignore_imag else a
return QPState(
spin=spin,
kpoint=kpoint,
band=band,
e0=self.read_e0(spin, ik_file, band),
qpe=ri(self._egw[spin, ik_file, band]),
qpe_diago=ri(self._en_qp_diago[spin, ik_file, band]),
# Note ib_gw index.
vxcme=self._vxcme[spin, ik_file, ib_gw],
sigxme=self._sigxme[spin, ik_file, ib_gw],
sigcmee0=ri(self._sigcmee0[spin, ik_file, ib_gw]),
vUme=self._vUme[spin, ik_file, ib_gw],
ze0=ri(self._ze0[spin, ik_file, ib_gw]),
)
def read_qpgaps(self):
"""Read the QP gaps. Returns [nsppol, nkibz] array with QP gaps in eV."""
return self.read_value("egwgap")
def read_ksgaps(self):
"""Read the KS gaps. Returns [nsppol, nkibz] array with KS gaps in eV."""
return self.read_value("e0gap")
def read_e0(self, spin, kfile, band):
return self.ks_bands.eigens[spin, kfile, band]
def read_sigmaw(self, spin, kpoint, band):
"""Returns the real and the imaginary part of the self energy."""
if not self.has_spfunc:
raise ValueError("%s does not contain spectral function data." % self.path)
ik = self.kpt2fileindex(kpoint)
# Must shift band index (see fortran code that allocates with mdbgw)
ib_gw = band - self.min_gwbstart
#ib_gw = band - self.gwbstart_sk[spin, self.gwkpt2seqindex(kpoint)]
return self._omega_r, self._sigxcme[spin,:, ik, ib_gw]
def read_spfunc(self, spin, kpoint, band):
"""
Returns the spectral function.
one/pi * ABS(AIMAG(Sr%sigcme(ib,ikibz,io,is))) /
( (REAL(Sr%omega_r(io)-Sr%hhartree(ib,ib,ikibz,is)-Sr%sigxcme(ib,ikibz,io,is)))**2 &
+(AIMAG(Sr%sigcme(ib,ikibz,io,is)))**2) / Ha_eV,&
"""
if not self.has_spfunc:
raise ValueError("%s does not contain spectral function data" % self.path)
ik = self.kpt2fileindex(kpoint)
# Must shift band index (see fortran code that allocates with mdbgw)
ib_gw = band - self.min_gwbstart
#ib_gw = band - self.gwbstart_sk[spin, self.gwkpt2seqindex(kpoint)]
aim_sigc = np.abs(self._sigcme[spin,:,ik,ib_gw].imag)
den = np.zeros(self.nomega_r)
for io, omega in enumerate(self._omega_r):
den[io] = (omega - self._hhartree[spin,ik,ib_gw,ib_gw].real - self._sigxcme[spin,io,ik,ib_gw].real) ** 2 + \
self._sigcme[spin,io,ik,ib_gw].imag ** 2
return self._omega_r, 1./np.pi * (aim_sigc/den)
def read_eigvec_qp(self, spin, kpoint, band=None):
"""
Returns <KS|QPState> for the given spin, kpoint and band.
If band is None, <KS_b|QP_{b'}> is returned.
"""
ik = self.kpt2fileindex(kpoint)
# <KS|QPState>
# TODO
#eigvec_qp = self.read_value("eigvec_qp", cmode="c")
# eigvec_qp(nbnds,nbnds,nkibz,nsppol))
eigvec_qp = self.read_variable("eigvec_qp")
if band is not None:
return eigvec_qp[spin, ik, :, band, 0] + 1j * eigvec_qp[spin, ik, :, band, 1]
else:
return eigvec_qp[spin, ik, :, :, 0] + 1j * eigvec_qp[spin, ik, :, :, 1]
def read_params(self):
"""
Read the parameters of the calculation.
Returns: OrderedDict with the value of the parameters.
"""
param_names = [
"ecutwfn", "ecuteps", "ecutsigx", "scr_nband", "sigma_nband",
"gwcalctyp", "scissor_ene",
]
# Read data and convert to scalar to avoid problems with pandas dataframes.
# Old sigres files may not have all the metadata.
params = OrderedDict()
for pname in param_names:
v = self.read_value(pname, default=None)
params[pname] = v if v is None else np.asarray(v).item()
# Other quantities that might be subject to convergence studies.
#params["nkibz"] = len(self.ibz)
return params
#def read_mlda_to_qp(self, spin, kpoint, band=None):
# """Returns the unitary transformation KS --> QPS"""
# ik = self.kpt2fileindex(kpoint)
# if band is not None:
# return self._mlda_to_qp[spin,ik,:,band]
# else:
# return self._mlda_to_qp[spin,ik,:,:]
#def read_qprhor(self):
# """Returns the QPState density in real space."""
[docs]class SigresRobot(Robot, RobotWithEbands):
"""
This robot analyzes the results contained in multiple SIGRES.nc files.
.. rubric:: Inheritance Diagram
.. inheritance-diagram:: SigresRobot
"""
# Try to have API similar to SigEPhRobot
EXT = "SIGRES"
def __init__(self, *args):
super().__init__(*args)
if len(self.abifiles) in (0, 1): return
# TODO
# Check dimensions and self-energy states and issue warning.
warns = []; wapp = warns.append
nc0 = self.abifiles[0]
same_nsppol, same_nkcalc = True, True
if any(nc.nsppol != nc0.nsppol for nc in self.abifiles):
same_nsppol = False
wapp("Comparing ncfiles with different values of nsppol.")
if any(nc.nkcalc != nc0.nkcalc for nc in self.abifiles):
same_nkcalc = False
wapp("Comparing ncfiles with different number of k-points in self-energy. Doh!")
if same_nsppol and same_nkcalc:
# FIXME
# Different values of bstart_ks are difficult to handle
# Because the high-level API assumes an absolute global index
# Should decide how to treat this case: either raise or interpret band as an absolute band index.
if any(np.any(nc.gwbstart_sk != nc0.gwbstart_sk) for nc in self.abifiles):
wapp("Comparing ncfiles with different values of gwbstart_sk")
if any(np.any(nc.gwbstop_sk != nc0.gwbstop_sk) for nc in self.abifiles):
wapp("Comparing ncfiles with different values of gwbstop_sk")
if warns:
for w in warns:
cprint(w, color="yellow")
def _check_dims_and_params(self):
"""Test that nsppol, sigma_kpoints, tlist are consistent."""
if not len(self.abifiles) > 1:
return 0
nc0 = self.abifiles[0]
errors = []
eapp = errors.append
if any(nc.nsppol != nc0.nsppol for nc in self.abifiles[1:]):
eapp("Files with different values of `nsppol`")
if any(nc.nkcalc != nc0.nkcalc for nc in self.abifiles[1:]):
eapp("Files with different values of `nkcalc`")
else:
for nc in self.abifiles[1:]:
for k0, k1 in zip(nc0.sigma_kpoints, nc.sigma_kpoints):
if k0 != k1:
eapp("Files with different values of `sigma_kpoints`")
if errors:
raise ValueError("Cannot compare multiple SIGRES.nc files. Reason:\n %s" % "\n".join(errors))
[docs] def merge_dataframes_sk(self, spin, kpoint, **kwargs):
for i, (label, sigr) in enumerate(self.items()):
frame = sigr.get_dataframe_sk(spin, kpoint, index=label)
if i == 0:
table = frame
else:
table = table.append(frame)
return table
[docs] def get_qpgaps_dataframe(self, spin=None, kpoint=None, with_geo=False, abspath=False, funcs=None, **kwargs):
"""
Return a |pandas-DataFrame| with the QP gaps for all files in the robot.
Args:
spin: Spin index.
kpoint
with_geo: True if structure info should be added to the dataframe
abspath: True if paths in index should be absolute. Default: Relative to getcwd().
funcs: Function or list of functions to execute to add more data to the DataFrame.
Each function receives a |SigresFile| object and returns a tuple (key, value)
where key is a string with the name of column and value is the value to be inserted.
"""
# TODO: Ideally one should select the k-point for which we have the fundamental gap for the given spin
# TODO: In principle the SIGRES might have different k-points
if spin is None: spin = 0
if kpoint is None: kpoint = 0
attrs = [
"nsppol",
#"nspinor", "nspden", #"ecut", "pawecutdg",
#"tsmear", "nkibz",
] + kwargs.pop("attrs", [])
rows, row_names = [], []
for label, sigres in self.items():
row_names.append(label)
d = OrderedDict()
for aname in attrs:
d[aname] = getattr(sigres, aname, None)
qpgap = sigres.get_qpgap(spin, kpoint)
d.update({"qpgap": qpgap})
# Add convergence parameters
d.update(sigres.params)
# Add info on structure.
if with_geo:
d.update(sigres.structure.get_dict4pandas(with_spglib=True))
# Execute functions.
if funcs is not None: d.update(self._exec_funcs(funcs, sigres))
rows.append(d)
row_names = row_names if not abspath else self._to_relpaths(row_names)
return pd.DataFrame(rows, index=row_names, columns=list(rows[0].keys()))
[docs] def get_fit_gaps_vs_ecuteps(self, spin, kpoint, plot_qpmks=True, slice_data=None, fontsize=12):
"""
Fit QP direct gaps as a function of ecuteps using Eq. 16 of http://dx.doi.org/10.1063/1.4900447
to extrapolate results for ecuteps --> +oo.
Args:
spin: Spin index (0 or 1)
kpoint: K-point in self-energy. Accepts |Kpoint|, vector or k-point index.
plot_qpmks: If False, plot QP_gap else (QP_gap - KS_gap) i.e. gap correction
slice_data: Python slice object. Used to downsample data points.
None to use all files of the SigResRobot.
fontsize: legend and label fontsize.
Return: TODO
"""
# Make sure that nsppol, sigma_kpoints are consistent.
self._check_dims_and_params()
# Get dimensions and index of the k-point in the sigma_nk array.
nc0 = self.abifiles[0]
nsppol, sigma_kpoints = nc0.nsppol, nc0.sigma_kpoints
ik = nc0.reader.gwkpt2seqindex(kpoint)
kgw = nc0.sigma_kpoints[ik]
# Order files by ecuteps
labels, ncfiles, params = self.sortby("ecuteps", unpack=True)
ecuteps_vals = np.array(params)
# Get QP and KS gaps ordered by ecuteps_vals.
qp_gaps, ks_gaps = map(np.array, zip(*[ncfile.get_qpgap(spin, kgw, with_ksgap=True)
for ncfile in ncfiles]))
ydata = qp_gaps if not plot_qpmks else qp_gaps - ks_gaps
if slice_data is not None:
# Allow user to select a subset of data points via python slice
ecuteps_vals = ecuteps_vals[slice_data]
ydata = ydata[slice_data]
# Fit results as a function of ecuteps
from scipy.optimize import curve_fit
def func(x, a, b, c):
return a * x**(-1.5) + b * x**(-2.5) + c
popt, pcov = curve_fit(func, ecuteps_vals, ydata)
ax, fig, plt = get_ax_fig_plt(ax=None)
ax.plot(ecuteps_vals, ydata, 'ro', label='ab-initio data')
min_ecuteps, max_ecuteps = ecuteps_vals.min(), ecuteps_vals.max() + 20
xs = np.linspace(min_ecuteps, max_ecuteps, num=50)
# Change label depending on plot_qpmks
what = r"\Delta E_g" if plot_qpmks else r"E_g"
ax.plot(xs, func(xs, *popt), 'b-',
label=f'fit: $B_3$=%5.3f, $B_5$=%5.3f, ${what} (\infty)$=%5.3f' % tuple(popt))
ax.hlines(popt[-1], min_ecuteps, max_ecuteps, color="k")
ax.legend(loc="best", fontsize=fontsize, shadow=True)
ax.grid(True)
ax.set_xlabel('ecuteps (Ha)')
ax.set_ylabel(f'${what}$ (eV)')
#ax.set_ylabel('$\Delta E(E_c^{\chi})$ (eV)')
#ax.title(r'$\Delta E(E_c^{\chi}) = \Delta E_g (\infty) + B_3 * E_c^{\chi (-3/2)} + B_5* E_c^{\chi (-5/2)} $')
#if show:
plt.show()
return dict2namedtuple(
fig=fig,
func=func,
ecuteps_vals=ecuteps_vals,
ydata=ydata,
popt=popt,
pcov=pcov,
)
# An alias to have a common API for robots.
get_dataframe = get_qpgaps_dataframe
[docs] @add_fig_kwargs
def plot_qpgaps_convergence(self, plot_qpmks=True, sortby=None, hue=None, sharey=False, fontsize=8, **kwargs):
"""
Plot the convergence of the direct QP gaps for all the k-points available in the robot.
Args:
plot_qpmks: If False, plot QP_gap, KS_gap else (QP_gap - KS_gap)
sortby: Define the convergence parameter, sort files and produce plot labels.
Can be None, string or function. If None, no sorting is performed.
If string and not empty it's assumed that the abifile has an attribute
with the same name and `getattr` is invoked.
If callable, the output of sortby(abifile) is used.
hue: Variable that define subsets of the data, which will be drawn on separate lines.
Accepts callable or string
If string, it's assumed that the abifile has an attribute with the same name and getattr is invoked.
If callable, the output of hue(abifile) is used.
sharey: True if y-axis should be shared.
fontsize: legend and label fontsize.
Returns: |matplotlib-Figure|
"""
# Make sure that nsppol, sigma_kpoints are consistent.
self._check_dims_and_params()
nc0 = self.abifiles[0]
nsppol, sigma_kpoints = nc0.nsppol, nc0.sigma_kpoints
# Build grid with (nkpt, 1) plots.
ncols, nrows = 1, len(sigma_kpoints)
ax_list, fig, plt = get_axarray_fig_plt(None, nrows=nrows, ncols=ncols,
sharex=True, sharey=sharey, squeeze=False)
ax_list = ax_list.ravel()
if hue is None:
labels, ncfiles, params = self.sortby(sortby, unpack=True)
else:
groups = self.group_and_sortby(hue, sortby)
for ik, (kgw, ax) in enumerate(zip(sigma_kpoints, ax_list)):
for spin in range(nsppol):
ax.set_title("QP dirgap k:%s" % (repr(kgw)), fontsize=fontsize)
# Extract QP dirgap for [spin, kgw, itemp]
if hue is None:
qp_gaps, ks_gaps = map(np.array, zip(*[ncfile.get_qpgap(spin, kgw, with_ksgap=True)
for ncfile in ncfiles]))
yvals = qp_gaps if not plot_qpmks else qp_gaps - ks_gaps
if not is_string(params[0]):
ax.plot(params, yvals, marker=nc0.marker_spin[spin])
else:
# Must handle list of strings in a different way.
xn = range(len(params))
ax.plot(xn, yvals, marker=nc0.marker_spin[spin])
ax.set_xticks(xn)
ax.set_xticklabels(params, fontsize=fontsize)
else:
for g in groups:
qp_gaps, ks_gaps = map(np.array, zip(*[ncfile.get_qpgap(spin, kgw, with_ksgap=True)
for ncfile in g.abifiles]))
yvals = qp_gaps if not plot_qpmks else qp_gaps - ks_gaps
label = "%s: %s" % (self._get_label(hue), g.hvalue)
ax.plot(g.xvalues, qp_gaps, marker=nc0.marker_spin[spin], label=label)
ax.grid(True)
if ik == len(sigma_kpoints) - 1:
ax.set_xlabel("%s" % self._get_label(sortby))
if sortby is None: rotate_ticklabels(ax, 15)
if ik == 0:
if plot_qpmks:
ax.set_ylabel("QP-KS direct gap (eV)", fontsize=fontsize)
else:
ax.set_ylabel("QP direct gap (eV)", fontsize=fontsize)
if hue is not None:
ax.legend(loc="best", fontsize=fontsize, shadow=True)
return fig
[docs] @add_fig_kwargs
def plot_qpdata_conv_skb(self, spin, kpoint, band, sortby=None, hue=None,
fontsize=8, **kwargs):
"""
For each file in the robot, plot the convergence of the QP results for given (spin, kpoint, band)
Args:
spin: Spin index.
kpoint: K-point in self-energy. Accepts |Kpoint|, vector or index.
band: Band index.
sortby: Define the convergence parameter, sort files and produce plot labels.
Can be None, string or function. If None, no sorting is performed.
If string and not empty it's assumed that the abifile has an attribute
with the same name and `getattr` is invoked.
If callable, the output of sortby(abifile) is used.
hue: Variable that define subsets of the data, which will be drawn on separate lines.
Accepts callable or string
If string, it's assumed that the abifile has an attribute with the same name and getattr is invoked.
If callable, the output of hue(abifile) is used.
what_list: List of strings selecting the quantity to plot.
fontsize: legend and label fontsize.
Returns: |matplotlib-Figure|
"""
# Make sure that nsppol and sigma_kpoints are consistent
self._check_dims_and_params()
# TODO: Add more quantities DW, Fan(0)
# TODO: Decide how to treat complex quantities, avoid annoying ComplexWarning
# TODO: Format for g.hvalue
# Treat fundamental gaps
# Quantities to plot.
what_list = ["re_qpe", "imag_qpe", "ze0"]
# Build grid plot.
nrows, ncols = len(what_list), 1
ax_list, fig, plt = get_axarray_fig_plt(None, nrows=nrows, ncols=ncols,
sharex=True, sharey=False, squeeze=False)
ax_list = ax_list.ravel()
nc0 = self.abifiles[0]
ik = nc0.reader.gwkpt2seqindex(kpoint)
kpoint = nc0.sigma_kpoints[ik]
# Sort and read QP data.
if hue is None:
labels, ncfiles, params = self.sortby(sortby, unpack=True)
qplist = [ncfile.reader.read_qp(spin, kpoint, band) for ncfile in ncfiles]
else:
groups = self.group_and_sortby(hue, sortby)
qplist_group = []
for g in groups:
lst = [ncfile.reader.read_qp(spin, kpoint, band) for ncfile in g.abifiles]
qplist_group.append(lst)
for i, (ax, what) in enumerate(zip(ax_list, what_list)):
if hue is None:
# Extract QP data.
yvals = [getattr(qp, what) for qp in qplist]
if not is_string(params[0]):
ax.plot(params, yvals, marker=nc0.marker_spin[spin])
else:
# Must handle list of strings in a different way.
xn = range(len(params))
ax.plot(xn, yvals, marker=nc0.marker_spin[spin])
ax.set_xticks(xn)
ax.set_xticklabels(params, fontsize=fontsize)
else:
for g, qplist in zip(groups, qplist_group):
# Extract QP data.
yvals = [getattr(qp, what) for qp in qplist]
label = "%s: %s" % (self._get_label(hue), g.hvalue)
ax.plot(g.xvalues, yvals, marker=nc0.marker_spin[spin], label=label)
ax.grid(True)
ax.set_ylabel(what)
if i == len(what_list) - 1:
ax.set_xlabel("%s" % self._get_label(sortby))
if sortby is None: rotate_ticklabels(ax, 15)
if i == 0 and hue is not None:
ax.legend(loc="best", fontsize=fontsize, shadow=True)
if "title" not in kwargs:
title = "QP results spin: %s, k:%s, band: %s" % (spin, repr(kpoint), band)
fig.suptitle(title, fontsize=fontsize)
return fig
[docs] @add_fig_kwargs
def plot_qpfield_vs_e0(self, field, sortby=None, hue=None, fontsize=8,
sharey=False, colormap="jet", e0="fermie", **kwargs):
"""
For each file in the robot, plot one of the attributes of :class:`QpState`
as a function of the KS energy.
Args:
field (str): String defining the attribute to plot.
sharey: True if y-axis should be shared.
.. note::
For the meaning of the other arguments, see other robot methods.
Returns: |matplotlib-Figure|
"""
import matplotlib.pyplot as plt
cmap = plt.get_cmap(colormap)
if hue is None:
ax_list = None
lnp_list = self.sortby(sortby)
for i, (label, ncfile, param) in enumerate(lnp_list):
if sortby is not None:
label = "%s: %s" % (self._get_label(sortby), param)
fig = ncfile.plot_qps_vs_e0(with_fields=list_strings(field),
e0=e0, ax_list=ax_list, color=cmap(i / len(lnp_list)), fontsize=fontsize,
sharey=sharey, label=label, show=False)
ax_list = fig.axes
else:
# group_and_sortby and build (ngroups,) subplots
groups = self.group_and_sortby(hue, sortby)
nrows, ncols = 1, len(groups)
ax_mat, fig, plt = get_axarray_fig_plt(None, nrows=nrows, ncols=ncols,
sharex=True, sharey=sharey, squeeze=False)
for ig, g in enumerate(groups):
subtitle = "%s: %s" % (self._get_label(hue), g.hvalue)
ax_mat[0, ig].set_title(subtitle, fontsize=fontsize)
for i, (nclabel, ncfile, param) in enumerate(g):
fig = ncfile.plot_qps_vs_e0(with_fields=list_strings(field),
e0=e0, ax_list=ax_mat[:, ig], color=cmap(i / len(g)), fontsize=fontsize,
sharey=sharey, label="%s: %s" % (self._get_label(sortby), param), show=False)
if ig != 0:
for ax in ax_mat[:, ig]:
set_visible(ax, False, "ylabel")
return fig
[docs] @add_fig_kwargs
def plot_selfenergy_conv(self, spin, kpoint, band, sortby=None, hue=None,
colormap="jet", xlims=None, fontsize=8, **kwargs):
"""
Plot the convergence of the e-e self-energy wrt to the ``sortby`` parameter.
Values can be optionally grouped by `hue`.
Args:
spin: Spin index.
kpoint: K-point in self-energy (can be |Kpoint|, list/tuple or int)
band: Band index.
sortby: Define the convergence parameter, sort files and produce plot labels.
Can be None, string or function. If None, no sorting is performed.
If string and not empty it's assumed that the abifile has an attribute
with the same name and `getattr` is invoked.
If callable, the output of sortby(abifile) is used.
hue: Variable that define subsets of the data, which will be drawn on separate lines.
Accepts callable or string
If string, it's assumed that the abifile has an attribute with the same name and getattr is invoked.
If callable, the output of hue(abifile) is used.
colormap: matplotlib color map.
xlims: Set the data limits for the x-axis. Accept tuple e.g. ``(left, right)``
or scalar e.g. ``left``. If left (right) is None, default values are used.
fontsize: Legend and title fontsize.
Returns: |matplotlib-Figure|
"""
# Make sure that nsppol and sigma_kpoints are consistent
self._check_dims_and_params()
import matplotlib.pyplot as plt
cmap = plt.get_cmap(colormap)
if hue is None:
ax_list = None
lnp_list = self.sortby(sortby)
for i, (label, ncfile, param) in enumerate(lnp_list):
sigma = ncfile.read_sigee_skb(spin, kpoint, band)
fig = sigma.plot(ax_list=ax_list,
label=label, color=cmap(i/len(lnp_list)), show=False)
ax_list = fig.axes
else:
# group_and_sortby and build (3, ngroups) subplots
groups = self.group_and_sortby(hue, sortby)
nrows, ncols = 3, len(groups)
ax_mat, fig, plt = get_axarray_fig_plt(None, nrows=nrows, ncols=ncols,
sharex=True, sharey=True, squeeze=False)
for ig, g in enumerate(groups):
subtitle = "%s: %s" % (self._get_label(hue), g.hvalue)
ax_mat[0, ig].set_title(subtitle, fontsize=fontsize)
for i, (nclabel, ncfile, param) in enumerate(g):
sigma = ncfile.read_sigee_skb(spin, kpoint, band)
fig = sigma.plot(ax_list=ax_mat[:, ig],
label="%s: %s" % (self._get_label(sortby), param),
color=cmap(i / len(g)), show=False)
if ig != 0:
for ax in ax_mat[:, ig]:
set_visible(ax, False, "ylabel")
for ax in ax_mat.ravel():
set_axlims(ax, xlims, "x")
return fig
[docs] def yield_figs(self, **kwargs): # pragma: no cover
"""
This function *generates* a predefined list of matplotlib figures with minimal input from the user.
"""
yield self.plot_qpgaps_convergence(plot_qpmks=True, show=False)
#yield self.plot_qpdata_conv_skb(spin, kpoint, band, show=False)
#yield self.plot_qpfield_vs_e0(field, show=False)
#yield self.plot_selfenergy_conv(spin, kpoint, band, sortby=None, hue=None, show=False)
[docs] def write_notebook(self, nbpath=None):
"""
Write a jupyter_ notebook to ``nbpath``. If nbpath is None, a temporay file in the current
working directory is created. Return path to the notebook.
"""
nbformat, nbv, nb = self.get_nbformat_nbv_nb(title=None)
args = [(l, f.filepath) for l, f in self.items()]
nb.cells.extend([
#nbv.new_markdown_cell("# This is a markdown cell"),
#nbv.new_code_cell("plotter = robot.get_ebands_plotter()"),
nbv.new_code_cell("robot = abilab.SigresRobot(*%s)\nrobot.trim_paths()\nrobot" % str(args)),
nbv.new_code_cell("robot.get_qpgaps_dataframe(spin=None, kpoint=None, with_geo=False)"),
nbv.new_code_cell("robot.plot_qpgaps_convergence(plot_qpmks=True, sortby=None, hue=None);"),
nbv.new_code_cell("#robot.plot_qpdata_conv_skb(spin=0, kpoint=0, band=0, sortby=None, hue=None);"),
nbv.new_code_cell("robot.plot_qpfield_vs_e0(field='qpeme0', sortby=None, hue=None);"),
nbv.new_code_cell("#robot.plot_selfenergy_conv(spin=0, kpoint=0, band=0, sortby=None, hue=None);"),
])
# Mixins
nb.cells.extend(self.get_baserobot_code_cells())
nb.cells.extend(self.get_ebands_code_cells())
return self._write_nb_nbpath(nb, nbpath)