Source code for solcore.material_system.material_system

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)