import math # hyperbolic functions etc in parameterisation
import os
import sys
from functools import lru_cache # cache function calls to stop things taking forever / recalculating smae things
import numpy as np
from scipy.integrate import quad
import configparser
import solcore
from solcore.material_system import critical_point_interpolate
from solcore.parameter_system import ParameterSystem
from solcore.constants import h, c, q, kb, pi, electron_mass as m0, vacuum_permittivity
from solcore.singleton import Singleton
from solcore.absorption_calculator.sopra_db import sopra_database, compounds_info
from solcore.absorption_calculator.nk_db import nkdb_load_n, nkdb_load_k
from solcore.material_data import calculate_mobility
[docs]class MaterialSystem(metaclass=Singleton):
""" The core class that manage the materials in solcore.
"""
def __init__(self, sources=None):
self.known_materials = {}
self.sources = {}
if sources is not None:
self.sources = {k.lower(): sources(k) for k in sources()}
[docs] def read(self, source, value):
""" Reads a new source. """
self.sources[source.lower()] = value
[docs] def material(self, name, sopra=False, nk_db=False):
""" This function checks if the requested material exists and creates a class
that contains its properties, assuming that the material does not exists in
the database, yet.
Such class will serve as the base class for all the derived materials based on that SpecificMaterial.
For example, N-type GaAs and P-type GaAs use the same SpecificMaterial, just with a different doping, and the
happens with GaAs at 300K or 200K.
The derived materials based on a SpecificMaterial are instances of the SpecificMaterial class.
>>> GaAs = solcore.material('GaAs') # The SpecificMaterial class
>>> n_GaAs = GaAs(Nd=1e23) # Instance of the class
>>> p_GaAs = GaAs(Na=1e22) # Another instance of GaAs with different doping
>>> AlGaAs = solcore.material('AlGaAs') # The SpecificMaterial class
>>> AlGaAs_1 = AlGaAs(Al=0.3) # Instance of the class. For compounds, the variable element MUST be present
>>> AlGaAs_2 = AlGaAs(Al=0.7, T=290) # Different composition and T (the default is T=300K)
The material is created from the parameters in the parameter_system and the n and k data if available. If the
n and k data does not exists - at all or for that composition - then n=1 and k=0 at all wavelengths. Keep in
mind that the available n and k data is valid only at room temperature.
:param name: Name of the material
:param sopra: If a SOPRA material must be used, rather than the normal database material, in case both exist.
:return: A class of that material
"""
suffix = ''
# First we check if the material exists. If not, some help is provided
try:
if sopra:
sopra_database(Material=name)
suffix = '_sopra'
elif nk_db:
suffix = '_nk'
else:
ParameterSystem().database.options(name)
except configparser.NoSectionError:
try:
sopra_database(Material=name)
sopra = True
suffix = '_sopra'
except:
valid_materials = sorted(ParameterSystem().database.sections())
valid_materials.remove('Immediate Calculables')
valid_materials.remove('Final Calculables')
print(
'\nMaterial ERROR: "{}" is not in the semiconductors database or in the SOPRA database. Valid semiconductor materials are: '.format(
name))
for v in valid_materials:
if "x" in ParameterSystem().database.options(v):
x = ParameterSystem().database.get(v, 'x')
print('\t {} \tx = {}'.format(v, x))
else:
print('\t {}'.format(v))
print(
'\nIn compounds, check that the order of the elements is the correct one (eg. GaInSb is OK but InGaSb'
' is not).')
val = input('\nDo you want to see the list of available SOPRA materials (y/n)?')
if val in 'Yy':
sopra_database.material_list()
sys.exit()
except solcore.absorption_calculator.sopra_db.SOPRAError:
pass
# Then we check if the material has already been created. If not, we create it.
if name + suffix in self.known_materials:
return self.known_materials[name + suffix]
elif sopra:
return self.sopra_material(name)
elif nk_db:
return self.nk_material(name)
else:
return self.parameterised_material(name)
[docs] def parameterised_material(self, name):
""" The function that actually creates the material class. """
if "x" in ParameterSystem().database.options(name):
self.composition = [ParameterSystem().database.get(name, "x")]
else:
self.composition = []
class SpecificMaterial(BaseMaterial):
material_string = name
composition = self.composition
def __init__(self, T=300, **kwargs):
BaseMaterial.__init__(self, T=T, **kwargs)
if name.lower() in self.sources.keys():
SpecificMaterial.material_directory = self.sources[name.lower()]
extension = ''
if len(SpecificMaterial.composition) == 0:
extension = '.txt'
SpecificMaterial.n_path = os.path.join(SpecificMaterial.material_directory, 'n' + extension)
SpecificMaterial.k_path = os.path.join(SpecificMaterial.material_directory, 'k' + extension)
SpecificMaterial.__name__ = name
self.known_materials[name] = SpecificMaterial
return SpecificMaterial
[docs] def sopra_material(self, name):
""" Creates an optical material fromt he SOPRA database. """
try:
self.composition = [compounds_info.get(name, "x")]
except:
self.composition = []
class SpecificMaterial(BaseMaterial):
material_string = name
composition = self.composition
data = sopra_database(Material=name)
def __init__(self, T=300, **kwargs):
BaseMaterial.__init__(self, T=T, **kwargs)
def __getattr__(self, attrname): # only used for unknown attributes
if attrname == "n":
return self.n_interpolated
if attrname == "k":
return self.k_interpolated
raise AttributeError(
'Parameter "{}" not available for this SOPRA material "{}".'.format(attrname, self.material_string))
@lru_cache(maxsize=1)
def load_n_data(self):
if len(self.composition) == 0:
wl, n = self.data.load_n()
self.n_data = np.vstack((wl * 1e-9, n))
@lru_cache(maxsize=1)
def load_k_data(self):
if len(self.composition) == 0:
wl, k = self.data.load_k()
self.k_data = np.vstack((wl * 1e-9, k))
def n_interpolated(self, x):
assert len(self.composition) <= 1, "Can't interpolate 2d spectra yet"
try:
self.load_n_data()
except:
print('Material "{}" does not have n-data defined. Returning "ones"'.format(self.material_string))
return np.ones_like(x)
if len(self.composition) == 0:
y = np.interp(x, self.n_data[0], self.n_data[1])
else:
wl, y, trash = self.data.load_composition(Lambda=x * 1e9,
**{self.composition[0]: self.main_fraction * 100})
return y
def k_interpolated(self, x):
assert len(self.composition) <= 1, "Can't interpolate 2d spectra yet"
try:
self.load_k_data()
except:
print('Material "{}" does not have k-data defined. Returning "zeroes"'.format(self.material_string))
return np.zeros_like(x)
if len(self.composition) == 0:
y = np.interp(x, self.k_data[0], self.k_data[1])
else:
wl, trash, y = self.data.load_composition(Lambda=x * 1e9,
**{self.composition[0]: self.main_fraction * 100})
return y
SpecificMaterial.__name__ = name + '_sopra'
self.known_materials[name + '_sopra'] = SpecificMaterial
return SpecificMaterial
[docs] def nk_material(self, name):
""" Creates an optical material from the refractiveindex.info database. """
class SpecificMaterial(BaseMaterial):
def __init__(self, T=300, **kwargs):
BaseMaterial.__init__(self, T=T, **kwargs)
def __getattr__(self, attrname): # only used for unknown attributes
if attrname == "n":
return self.n_interpolated
if attrname == "k":
return self.k_interpolated
raise AttributeError(
'Parameter "{}" not available for this refractiveindex.info material "{}".'.format(attrname, self.material_string))
@lru_cache(maxsize=1)
def load_n_data(self):
wl, n = nkdb_load_n(name)
self.n_data = np.vstack((wl * 1e-6, n)) # wavelengths in DB are in microns
@lru_cache(maxsize=1)
def load_k_data(self):
wl, k = nkdb_load_k(name)
self.k_data = np.vstack((wl * 1e-6, k))
def n_interpolated(self, x):
assert len(self.composition) <= 1, "Can't interpolate 2d spectra yet"
try:
self.load_n_data()
except:
print('Material "{}" does not have n-data defined. Returning "ones"'.format(self.material_string))
return np.ones_like(x)
y = np.interp(x, self.n_data[0], self.n_data[1])
return y
def k_interpolated(self, x):
assert len(self.composition) <= 1, "Can't interpolate 2d spectra yet"
try:
self.load_k_data()
except:
print('Material "{}" does not have k-data defined. Returning "zeroes"'.format(self.material_string))
return np.zeros_like(x)
y = np.interp(x, self.k_data[0], self.k_data[1])
return y
SpecificMaterial.__name__ = name + '_nk'
self.known_materials[name + '_nk'] = SpecificMaterial
return SpecificMaterial
[docs]class BaseMaterial:
""" The solcore base material class
"""
material_string = "Unnamed"
composition = []
main_fraction = 0
material_directory = None
k_path = None
n_path = None
strained = False
def __init__(self, T, **kwargs):
self.Na = kwargs["Na"] if "Na" in kwargs else 1
self.Nd = kwargs["Nd"] if "Nd" in kwargs else 1
self.__dict__.update(kwargs)
self.T = T
for element in self.composition:
setattr(self, element, kwargs[element])
if len(self.composition) != 0:
self.main_fraction = kwargs[self.composition[0]]
self.key_parameters = sorted(list(kwargs.keys()))
def __getattr__(self, attrname): # only used for unknown attributes.
if attrname == "n":
try:
return self.__getattribute__(attrname)
except AttributeError:
return self.n_interpolated
if attrname == "k":
try:
return self.__getattribute__(attrname)
except AttributeError:
return self.k_interpolated
if attrname == "electron_affinity":
try:
return ParameterSystem().get_parameter(self.material_string, attrname)
except:
# from http://en.wikipedia.org/wiki/Anderson's_rule and GaAs values
return (0.17 + 4.59) * q - self.valence_band_offset - self.band_gap
if attrname == "electron_mobility":
try:
return ParameterSystem().get_parameter(self.material_string, attrname)
except:
return calculate_mobility(self.material_string, False, self.Nd, self.main_fraction)
if attrname == "hole_mobility":
try:
return ParameterSystem().get_parameter(self.material_string, attrname)
except:
return calculate_mobility(self.material_string, True, self.Na, self.main_fraction)
if attrname == "Nc":
# return 2 * (2 * pi * self.eff_mass_electron_Gamma * m0 * kb * self.T / h ** 2) ** 1.5
# Changed by Diego 22/10/18: some materials dont have _Gamma but all have the normal electron effective mass
return 2 * (2 * pi * self.eff_mass_electron * m0 * kb * self.T / h ** 2) ** 1.5
if attrname == "Nv":
# Strictly speaking, this is valid only for III-V, zinc-blend semiconductors
mhh = self.eff_mass_hh_z
mlh = self.eff_mass_lh_z
Nvhh = 2 * (2 * pi * mhh * m0 * kb * self.T / h ** 2) ** 1.5
Nvlh = 2 * (2 * pi * mlh * m0 * kb * self.T / h ** 2) ** 1.5
return Nvhh + Nvlh
if attrname == "ni":
return np.sqrt(self.Nc * self.Nv * np.exp(-self.band_gap / (kb * self.T)))
if attrname == "radiative_recombination":
inter = lambda E: self.n(E) ** 2 * self.alphaE(E) * np.exp(-E / (kb * self.T)) * E ** 2
upper = self.band_gap + 10 * kb * self.T
return 1.0 / self.ni ** 2 * 2 * pi / (h ** 3 * c ** 2) * quad(inter, 0, upper)[0]
if attrname == "permittivity":
return self.relative_permittivity * vacuum_permittivity
kwargs = {element: getattr(self, element) for element in self.composition}
kwargs["T"] = self.T
return ParameterSystem().get_parameter(self.material_string, attrname, **kwargs)
[docs] @lru_cache(maxsize=1)
def load_n_data(self):
if len(self.composition) == 0:
self.n_data = np.loadtxt(self.n_path, unpack=True)
else:
self.n_data, self.n_critical_points = critical_point_interpolate.load_data_from_directory(self.n_path)
[docs] @lru_cache(maxsize=1)
def load_k_data(self):
if len(self.composition) == 0:
self.k_data = np.loadtxt(self.k_path, unpack=True)
else:
self.k_data, self.k_critical_points = critical_point_interpolate.load_data_from_directory(self.k_path)
[docs] def n_interpolated(self, x):
assert len(self.composition) <= 1, "Can't interpolate 2d spectra yet"
try:
self.load_n_data()
except:
print('Material "{}" does not have n-data defined. Returning "ones"'.format(self.material_string))
return np.ones_like(x)
if len(self.composition) == 0:
y = np.interp(x, self.n_data[0], self.n_data[1])
else:
x, y, self.critical_points_n = critical_point_interpolate.critical_point_interpolate(
self.n_data,
self.n_critical_points,
self.main_fraction,
x
)
return y
[docs] def k_interpolated(self, x):
assert len(self.composition) <= 1, "Can't interpolate 2d spectra yet"
try:
self.load_k_data()
except:
print('Material "{}" does not have k-data defined. Returning "zeros"'.format(self.material_string))
return np.zeros_like(x)
if len(self.composition) == 0:
y = np.interp(x, self.k_data[0], self.k_data[1])
else:
x, y, self.critical_points_k = critical_point_interpolate.critical_point_interpolate(
self.k_data,
self.k_critical_points,
self.main_fraction,
x
)
return y
[docs] def get(self, parameter):
return getattr(self, parameter)
[docs] def alpha(self, wavelength):
return 4 * math.pi * self.k(wavelength) / wavelength
[docs] def alphaE(self, energy):
return self.alpha(h * c / energy)
[docs] def latex_string(self):
s = self.material_string
for element in self.composition:
if self.__dict__[element] != 0:
s = s.replace(element, "{}_{{{:.3f}}}").format(element, self.__dict__[element])
else:
s = s.replace(element, "")
return "$\mathrm{{{}}}$".format(s)
[docs] def plain_string(self):
s = self.material_string
for element in self.composition:
if self.__dict__[element] != 0:
s = s.replace(element, "{}{:.3f}").format(element, self.__dict__[element])
else:
s = s.replace(element, "")
return "{}".format(s)
[docs] def html_string(self):
s = self.material_string
for element in self.composition:
if self.__dict__[element] != 0:
s = s.replace(element, "{}<sub>{:.3f}</sub>").format(element, self.__dict__[element])
else:
s = s.replace(element, "")
return "{}".format(s)
def __repr__(self):
parameters = ["{}={}".format(key, getattr(self, key)) for key in self.key_parameters]
parameters_string = " ".join(parameters)
return "<'{}' material{}>".format(
self.material_string,
"" if len(self.key_parameters) == 0 else " " + parameters_string)