"""
two-probe transport calculator
"""
from matplotlib import pyplot as plt
from nanotools.base import Base, Quantity
from nanotools.utils import to_quantity, ureg
from nanotools.current import Current
from nanotools.extpot import Region
from nanotools.twoprobe import TwoProbe
from nanotools.utils import dict_converter
from shutil import copyfile
from typing import List, Union
import attr
import numpy as np
import os
[docs]
@attr.s
class IV(Base):
"""Calculator that automates the workflow of computing charge currents
under a series of bias/gate voltages.
Attributes:
reference_calculator: instance of TwoProbe
temperature
currents: where results are stored
k_grid_4_current: a denser k-point grid for current computation.
work_func: work function. used in case of varying gate voltage.
voltages: the varying voltage values
varying_elements: list of Region objects or strings indicating what voltages are to be varied.
The voltages associated with all the elements in the list are varied simultaneously.
If a string is found in the list, the drain-source voltage is to be varied.
eg. [extpot.gates[0], extpot.gates[2], "drain-source"]. In this case, Vds=Vg1=Vg2.
"""
reference_calculator: TwoProbe = attr.ib(
converter=lambda d: dict_converter(d, TwoProbe),
validator=attr.validators.instance_of(TwoProbe),
)
voltages: Quantity = attr.ib(
default=None,
converter=attr.converters.optional(lambda x: to_quantity(x, "eV", shape=(-1))),
validator=attr.validators.instance_of(Quantity),
)
root: str = attr.ib(default=None)
sub_dirs: List[str] = attr.ib(factory=list)
k_grid_4_current = attr.ib(default=None)
temperature: float = attr.ib(
default=300.0, # default="kelvin"
converter=float,
validator=attr.validators.instance_of(float),
)
currents: Quantity = attr.ib(
default=None,
converter=attr.converters.optional(
lambda x: to_quantity(x, "ampere", shape=(-1))
),
# validator=attr.validators.instance_of(Quantity),
)
work_func: Quantity = attr.ib(
default=0.0 * ureg.eV,
converter=lambda x: to_quantity(x, "eV"),
validator=attr.validators.instance_of(Quantity),
)
varying_elements: List[Union[str, Region]] = attr.ib(
factory=list
) # pointers to reference_calculator..gates[?]
def __attrs_post_init__(self):
dev = self.reference_calculator.copy()
self.reference_calculator = dev.copy()
self._link_elements()
if self.root is None:
self.root = os.getcwd()
if len(self.sub_dirs) != len(self.voltages):
self.mk_sub_dir()
[docs]
def set_voltages(self, voltages, work_func=None):
"""sets voltage values and optionally the work function for gates
Args:
voltages (iterable[float]):
work_func (float/None): work function
"""
if work_func is not None:
self.work_func = work_func
self.voltages = voltages
self.mk_sub_dir()
[docs]
def set_varying_elements(self, elements):
"""sets the elements to which the varying voltage is to be applied
Args:
elements (List[Region/string])
"""
self.varying_elements = elements
self._link_elements()
def set_temperature(self, T):
self.temperature = T
def mk_sub_dir(self, dirs=None):
if dirs is None:
self.sub_dirs = []
for v in self.voltages:
self.sub_dirs.append(self._get_sub_dir(v))
else:
self.sub_dirs = dirs.copy()
for d in self.sub_dirs:
if not os.path.isdir(d):
os.mkdir(d)
[docs]
def solve(self, output="nano_iv_out"):
"""carries out self-consistent and charge current calculations at each given voltage.
Several directories will be spawned, holding input and output files for each corresponding
voltage value. For example, if device/ is the current work directory and the voltages are
[0.1,0.2,0.3], then the following directories will be spawned in parallel
device0.1/ device0.2/ device0.3/
Find results in self.currents
"""
self.gen_input_files()
print("preparation done. computation starts ...")
print(
"You may interrupt here and run scf.py and current.py under each of the i_v_.. directories."
)
self.scf()
self.calc_currents()
self.write(filename=output + ".json")
[docs]
def scf(self):
"""Enters each of the spawned directories and launch nanodcalplus
for self-consistent calculation
"""
for nwd in self.sub_dirs:
os.chdir(nwd)
command, binname = self.reference_calculator.center.solver.cmd.get_cmd(
"2prb"
)
ret = command("nano_2prb_in.json")
os.chdir(self.root)
[docs]
def calc_currents(self):
"""Enters each of the spawned directories and carries out charge
current calculation.
"""
ispin = self.reference_calculator.center.system.hamiltonian.ispin
if ispin == 2:
self.currents = np.zeros(shape=(len(self.voltages), 2))
else:
self.currents = 0 * np.array(self.voltages)
for i in range(len(self.sub_dirs)):
os.chdir(self.sub_dirs[i])
dev = TwoProbe.read(filename="nano_2prb_out.json")
cal = Current.from_twoprobe(twoprb=dev)
if self.k_grid_4_current is not None:
cal.center.system.kpoint.set_grid(self.k_grid_4_current)
if ispin == 2:
(
self.currents[i, 0],
self.currents[i, 1],
) = cal.calc_charge_current(T=self.temperature)
else:
self.currents[i] = cal.calc_charge_current(T=self.temperature)
os.chdir(self.root)
return self.currents
[docs]
def get_currents(self):
"""collects results from each spawned directory"""
ispin = self.reference_calculator.center.system.hamiltonian.ispin
if ispin == 2:
self.currents = np.zeros(shape=(len(self.voltages), 2))
else:
self.currents = 0 * np.array(self.voltages)
for i in range(len(self.sub_dirs)):
os.chdir(self.sub_dirs[i])
cal = Current.read(filename="nano_trsm_out.json")
if ispin == 2:
(
self.currents[i, 0],
self.currents[i, 1],
) = cal.get_charge_current(T=self.temperature)
else:
self.currents[i] = cal.get_charge_current(T=self.temperature)
os.chdir(self.root)
return self.currents
def _get_varying_voltage(self):
vals = self.voltages.copy()
return vals
def _get_sub_dir(self, v):
return os.path.join(self.root, "i_v_" + str(v))
def _link_elements(self):
for i in range(len(self.varying_elements)):
ele = self.varying_elements[i]
if isinstance(ele, dict):
self.varying_elements[i] = Region(**ele)
for i in range(len(self.varying_elements)):
ele = self.varying_elements[i]
if isinstance(ele, Region):
f = self._find_exist_gate(ele)
if f == -1:
self.reference_calculator.center.system.add_gate(region=ele)
self.varying_elements[i] = (
self.reference_calculator.center.system.hamiltonian.extpot.gates[f]
)
[docs]
def plot(self, log=False, filename=None, show=True):
"""plots I-V curve"""
self.set_units("si")
ispin = self.reference_calculator.center.system.hamiltonian.ispin
vals = self._get_varying_voltage()
if log:
currents = np.abs(self.currents)
else:
currents = self.currents.copy()
fig = plt.figure()
if ispin == 2:
plt.plot(vals.m, currents.m[:, 0], "-k^", label="spin-majority")
plt.plot(vals.m, currents.m[:, 1], "-kv", label="spin-minority")
plt.legend()
else:
plt.plot(vals.m, currents.m, "-ko")
if log:
plt.yscale("log")
plt.xlabel("Voltage (V)")
plt.ylabel(f"Current ({self.currents.u})")
fig.tight_layout()
if show:
plt.show()
if filename is not None:
fig.savefig(filename)
return fig
def _find_exist_gate(self, gate):
existed_gates = self.reference_calculator.center.system.hamiltonian.extpot.gates
if len(existed_gates) == 0 or existed_gates is None:
return -1
i = -1
c = 0
for gex in existed_gates:
if (
gex.fractional_x_range == gate.fractional_x_range
and gex.fractional_y_range == gate.fractional_y_range
and gex.fractional_z_range == gate.fractional_z_range
):
i = c
break
c = c + 1
return i
def _write_py_scf(self, dir):
py = """
from nanotools.twoprobe import TwoProbe
calc = TwoProbe.read(filename='nano_2prb_in.json')
calc.solve()
"""
with open(os.path.join(dir, "scf.py"), "w") as f:
f.write(py)
def _write_py_current(self, dir):
py = """
from nanotools import Current, TwoProbe
from nanotools.utils import to_quantity
dev = TwoProbe.read(filename="nano_2prb_out.json")
cal = Current.from_twoprobe(twoprb=dev)
cal.temperature = to_quantity(%s, "kelvin")
# don't forget to set a denser k-point mesh:
# cal.center.system.kpoint.set_grid(k_grid_4_current)
ispin = dev.center.system.hamiltonian.ispin
if ispin == 2:
Iu, Id = cal.calc_charge_current().m
unit = cal.calc_charge_current().u
print(f"majority spin current = {Iu}")
print(f"minority spin current = {Id}")
print("unit = ", unit)
else:
Iu = cal.calc_charge_current().m
unit = cal.calc_charge_current().u
print(f"current = {Iu}")
print("unit = ", unit)
"""
with open(os.path.join(dir, "current.py"), "w") as f:
f.write(py % self.temperature)
def copy_or_link(src, dst):
if os.path.exists(dst):
os.remove(dst)
try:
os.symlink(src, dst)
except OSError:
print("symlink fails. use copy instead.")
copyfile(src, dst)
except:
print("copy_or_link fails.")