"""Collection of functions to calculate LETID or B-O LID defect states, defect state transitions,
and device degradation given device details
"""
import numpy as np
import pandas as pd
import os
from scipy.constants import convert_temperature, elementary_charge, Boltzmann
from scipy.integrate import simpson
import datetime
import pvlib
from pvdeg import collection, utilities, standards, DATA_DIR
[docs]
def tau_now(tau_0, tau_deg, n_b):
"""
Return carrier lifetime of a LID or LETID-degraded wafer given initial lifetime, fully
degraded lifetime, and fraction of defects in recombination-active state B
Parameters
----------
tau_0 : numeric
Initial lifetime. Typcially in seconds, milliseconds, or microseconds.
tau_deg : numeric
Lifetime when wafer is fully-degraded, i.e. 100% of defects are in state B. Same units
as tau_0.
n_B : numeric
Percentage of defects in state B [%].
Returns
-------
numeric
lifetime of wafer with n_B% of defects in state B. Same units as tau_0 and tau_deg.
"""
return tau_0 / ((tau_0 / tau_deg - 1) * n_b / 100 + 1)
[docs]
def k_ij(attempt_frequency, activation_energy, temperature):
"""
Calculates an Arrhenius rate constant given attempt frequency, activation energy, and
temperature
Parameters
----------
attempt_frequency : numeric
Arrhenius pre-exponential factor (or attempt frequency) [s^-1].
activation_energy : numeric
Arrhenius activation energy [eV].
temperature : numeric
Temperature [C].
Returns
-------
reaction_rate : numeric
Arrhenius reaction rate constant [s^-1].
"""
temperature = convert_temperature(temperature, "C", "K")
q = elementary_charge
k = Boltzmann
reaction_rate = attempt_frequency * np.exp(
(-activation_energy * q) / (k * temperature)
)
return reaction_rate
[docs]
def carrier_factor(
tau,
transition,
temperature,
suns,
jsc,
wafer_thickness,
s_rear,
mechanism_params,
dn_lit=None,
):
"""
Return the delta_n^x_ij term to modify attempt frequency by excess carrier density. See
McPherson 2022 [1]_. Requires mechanism_params, a dict of required mechanism parameters.
Parameters
----------
tau : numeric
Carrier lifetime [us].
transition : str
Transition in the 3-state defect model (A <-> B <-> C). Must be 'ab', 'bc', or 'ba'.
temperature : numeric
Temperature [C].
suns : numeric
Applied injection level of device in terms of "suns", e.g. 1 for a device held at 1-sun
Jsc current injection in the dark, or at open-circuit with 1-sun illumination.
jsc : numeric
Short-circuit current density [mA/cm^2].
wafer_thickness : numeric
Wafer thickness [um].
s_rear : numeric
Rear surface recombination velocity [cm/s].
mechanism_params : dict
Dictionary of mechanism parameters.
These are typically taken from literature studies of transtions in the 3-state model.
They allow for calculation the excess carrier density of literature experiments (dn_lit)
Parameters are coded in 'kinetic_parameters.json'.
dn_lit : numeric, default None
Optional, supply in lieu of a complete set of mechanism_params if experimental dn_lit
is known.
Returns
-------
numeric
dn^x_ij term. Modified by the ratio of modeled dn to literature experiment
dn (dn/dn_lit).
References
----------
.. [1] A. N. McPherson, J. F. Karas, D. L. Young, and I. L. Repins,
“Excess carrier concentration in silicon devices and wafers: How bulk properties are
expected to accelerate light and elevated temperature degradation,”
MRS Advances, vol. 7, pp. 438–443, 2022, doi: 10.1557/s43580-022-00222-5.
"""
q = elementary_charge
if transition == "ab":
exponent = mechanism_params[f"x_{transition}"]
if dn_lit is None:
meas_tau = mechanism_params[f"tau_{transition}"]
meas_temp = mechanism_params[f"temperature_{transition}"]
meas_temp = convert_temperature(
meas_temp, "K", "C"
) # convert Kelvin to Celsius
meas_suns = mechanism_params[f"suns_{transition}"]
meas_jsc = 40
meas_wafer_thickness = mechanism_params[f"thickness_{transition}"]
meas_srv = mechanism_params[f"srv_{transition}"]
meas_structure = mechanism_params[f"structure_{transition}"]
if meas_structure == "wafer":
# unit conversions on jsc, tau, and wafer thickness:
dn_lit = (
((jsc * 0.001 * 10000 * meas_suns) * (meas_tau * 1e-6))
/ (meas_wafer_thickness * 1e-6)
/ q
)
else:
dn_lit = calc_dn(
meas_tau,
meas_temp,
meas_suns,
meas_jsc,
wafer_thickness=meas_wafer_thickness,
s_rear=meas_srv,
)
elif transition == "bc":
exponent = mechanism_params[f"x_{transition}"]
if dn_lit is None:
meas_tau = mechanism_params[f"tau_{transition}"]
meas_temp = mechanism_params[f"temperature_{transition}"]
meas_temp = convert_temperature(
meas_temp, "K", "C"
) # convert Kelvin to Celsius
meas_suns = mechanism_params[f"suns_{transition}"]
meas_jsc = 40
meas_wafer_thickness = mechanism_params[f"thickness_{transition}"]
meas_srv = mechanism_params[f"srv_{transition}"]
meas_structure = mechanism_params[f"structure_{transition}"]
if meas_structure == "wafer":
# unit conversions on jsc, tau, and wafer thickness:
dn_lit = (
((jsc * 0.001 * 10000 * meas_suns) * (meas_tau * 1e-6))
/ (meas_wafer_thickness * 1e-6)
/ q
)
else:
dn_lit = calc_dn(
meas_tau,
meas_temp,
meas_suns,
meas_jsc,
wafer_thickness=meas_wafer_thickness,
s_rear=meas_srv,
)
elif transition == "ba":
exponent = mechanism_params[f"x_{transition}"]
if dn_lit is None:
dn_lit = 1e21 # this is hardcoded
else:
exponent = 0
# or we could raise a ValueError and say transition has to be 'ab'|'bc'|'ba'
dn_lit = 1e21
dn = calc_dn(tau, temperature, suns, jsc, wafer_thickness, s_rear)
return (dn / dn_lit) ** exponent
[docs]
def carrier_factor_wafer(
tau, transition, suns, jsc, wafer_thickness, mechanism_params, dn_lit=None
):
r"""
Return the delta_n^x_ij term to modify attempt frequency by excess carrier density for a
passivated wafer, rather than a solar cell.
For a passivated wafer, delta_n increases linearly with lifetime:
.. math::
\Delta n = \tau*J/W
See McPherson 2022 [1]_. Requires mechanism_params, a dict of required mechanism parameters.
See 'MECHANISM_PARAMS' dict
Parameters
----------
tau : numeric
Carrier lifetime [us].
transition : str
Transition in the 3-state defect model (A <-> B <-> C). Must be 'ab', 'bc', or 'ba'.
suns : numeric
Applied injection level of device in terms of "suns", e.g. 1 for a device held at 1-sun
Jsc current injection in the dark, or at open-circuit with 1-sun illumination.
jsc : numeric
Short-circuit current density [mA/cm^2].
wafer_thickness : numeric
Wafer thickness [um].
mechanism_params : dict
Dictionary of mechanism parameters.
These are typically taken from literature studies of transtions in the 3-state model.
They allow for calculation the excess carrier density of literature experiments (dn_lit)
Parameters are coded in 'MECHANISM_PARAMS' dict.
dn_lit : numeric, default None
Optional, supply in lieu of a complete set of mechanism_params if experimental dn_lit
is known.
Returns
-------
numeric
dn^x_ij term. Modified by the ratio of modeled dn to literature experiment dn
(dn/dn_lit).
References
----------
.. [1] A. N. McPherson, J. F. Karas, D. L. Young, and I. L. Repins,
“Excess carrier concentration in silicon devices and wafers: How bulk properties are
expected to accelerate light and elevated temperature degradation,”
MRS Advances, vol. 7, pp. 438–443, 2022, doi: 10.1557/s43580-022-00222-5.
"""
q = elementary_charge
if transition == "ab":
exponent = mechanism_params[f"x_{transition}"]
if dn_lit is None:
meas_tau = mechanism_params[f"tau_{transition}"]
meas_temp = mechanism_params[f"temperature_{transition}"]
meas_temp = convert_temperature(
meas_temp, "K", "C"
) # convert Kelvin to Celsius
meas_suns = mechanism_params[f"suns_{transition}"]
meas_jsc = 40
meas_wafer_thickness = mechanism_params[f"thickness_{transition}"]
meas_srv = mechanism_params[f"srv_{transition}"]
meas_structure = mechanism_params[f"structure_{transition}"]
if meas_structure == "wafer":
# unit conversions on jsc, tau, and wafer thickness:
dn_lit = (
((jsc * 0.001 * 10000 * meas_suns) * (meas_tau * 1e-6))
/ (meas_wafer_thickness * 1e-6)
/ q
)
else:
dn_lit = calc_dn(
meas_tau,
meas_temp,
meas_suns,
meas_jsc,
wafer_thickness=meas_wafer_thickness,
s_rear=meas_srv,
)
elif transition == "bc":
exponent = mechanism_params[f"x_{transition}"]
if dn_lit is None:
meas_tau = mechanism_params[f"tau_{transition}"]
meas_temp = mechanism_params[f"temperature_{transition}"]
meas_temp = convert_temperature(
meas_temp, "K", "C"
) # convert Kelvin to Celsius
meas_suns = mechanism_params[f"suns_{transition}"]
meas_jsc = 40
meas_wafer_thickness = mechanism_params[f"thickness_{transition}"]
meas_srv = mechanism_params[f"srv_{transition}"]
meas_structure = mechanism_params[f"structure_{transition}"]
if meas_structure == "wafer":
# unit conversions on jsc, tau, and wafer thickness:
dn_lit = (
((jsc * 0.001 * 10000 * meas_suns) * (meas_tau * 1e-6))
/ (meas_wafer_thickness * 1e-6)
/ q
)
else:
dn_lit = calc_dn(
meas_tau,
meas_temp,
meas_suns,
meas_jsc,
wafer_thickness=meas_wafer_thickness,
s_rear=meas_srv,
)
elif transition == "ba":
exponent = mechanism_params[f"x_{transition}"]
if dn_lit is None:
dn_lit = 1e21 # this is hardcoded
else:
exponent = 0 # or we could raise a ValueError and say transition has to be 'ab'|'bc'|'ba'
dn_lit = 1e21
dn = ((jsc * 0.001 * 10000 * suns) * (tau * 1e-6)) / (wafer_thickness * 1e-6) / q
return (dn / dn_lit) ** exponent
[docs]
def calc_dn(
tau,
temperature,
suns,
jsc,
wafer_thickness,
s_rear,
na=7.2e21,
xp=0.00000024,
e_mobility=0.15,
nc=2.8e25,
nv=1.6e25,
e_g=1.79444e-19,
):
"""
Return excess carrier concentration, i.e. "injection", given lifetime, temperature,
suns-equivalent applied injection, and cell parameters
Parameters
----------
tau : numeric
Carrier lifetime [us].
temperature : numeric
Cell temperature [K].
suns : numeric
Applied injection level of device in terms of "suns",
e.g. 1 for a device held at 1-sun Jsc current injection.
jsc : numeric
Short-circuit current density of the cell [mA/cm^2].
wafer_thickness : numeric
Wafer thickness [um].
s_rear : numeric
Rear surface recombination velocity [cm/s].
na : numeric, default 7.2e21
Doping density [m^-3].
xp : numeric, default 0.00000024
width of the depletion region [m]. Treated as fixed width, as it is very small compared
to the bulk, so injection-dependent variations will have very small effects.
e_mobility : numeric, default 0.15
electron mobility [m^2/V-s].
nc : numeric, default 2.8e25
density of states of the conduction band [m^-3]
nv : numeric, default 1.6e25
density of states of the valence band [m^-3]
e_g : numeric, default 1.79444e-19
bandgap of silicon [J].
Returns
-------
dn : numeric
excess carrier concentration [m^-3]
"""
k = Boltzmann
q = elementary_charge
# unit conversions
tau = tau * 1e-6 # convert microseconds to seconds
temperature = convert_temperature(
temperature, "C", "K"
) # convert Celsius to Kelvin
jsc = jsc * 0.001 * 10000 # convert mA/cm^2 to A/m^2
wafer_thickness = wafer_thickness * 1e-6 # convert microns to meters
s_rear = s_rear / 100 # convert cm/s to m/s
i_applied = suns * jsc
v_applied = convert_i_to_v(tau, na, i_applied, wafer_thickness, s_rear, temperature)
diffusivity = e_mobility * k * temperature / q
ni2 = nc * nv * np.exp(-e_g / (k * temperature)) # ni^2 = Nc*Nv*exp(-Eg/kT)
arg = (wafer_thickness - xp) / (diffusivity * tau) ** 0.5
exp_prefactor = ni2 / na * np.exp(q * v_applied / (k * temperature))
cosh = np.cosh(arg)
sinh = np.sinh(arg)
numerator = (s_rear / diffusivity) * cosh + sinh * ((diffusivity * tau) ** (-0.5))
denominator = cosh * ((diffusivity * tau) ** (-0.5)) + (s_rear / diffusivity) * sinh
a_p = -exp_prefactor * numerator / denominator
b_p = exp_prefactor
dn = (((diffusivity * tau) ** (0.5)) / (wafer_thickness - xp)) * (
a_p * (cosh - 1) + b_p * sinh
)
return dn
[docs]
def convert_i_to_v(
tau,
na,
current,
wafer_thickness,
srv,
temperature=298.15,
e_mobility=0.15,
xp=0.00000024,
nc=2.8e25,
nv=1.6e25,
e_g=1.79444e-19,
):
"""
Return voltage given lifetime and applied current, and cell parameters
Parameters
----------
tau : numeric
Carrier lifetime [s].
na : numeric
Doping density [m^-3].
current : numeric
applied current [A].
wafer_thickness : numeric
Wafer thickness [m].
srv : numeric
Surface recombination velocity [m/s].
temperature : numeric, default 298.15
Cell temperature [K]
e_mobility : numeric, default 0.15
electron mobility [m^2/V-s].
xp : numeric, default 0.00000024
width of the depletion region [m]. Treated as fixed width, as it is very small compared
to the bulk, so injection-dependent variations will have very small effects.
nc : numeric, default 2.8e25
density of states of the conduction band [m^-3]
nv : numeric, default 1.6e25
density of states of the valence band [m^-3]
e_g : numeric, default 1.79444e-19
bandgap of silicon [J].
Returns
-------
voltage : numeric
cell voltage [V]
"""
k = Boltzmann
q = elementary_charge
diffusivity = e_mobility * k * temperature / q
diffusion_length = np.sqrt(diffusivity * tau)
ni2 = nc * nv * np.exp(-e_g / (k * temperature)) # ni^2 = Nc*Nv*exp(-Eg/kT)
arg = (wafer_thickness - xp) / diffusion_length
j0 = j0_gray(ni2, diffusivity, na, diffusion_length, arg, srv)
if current > 0:
voltage = (k * temperature / q) * np.log(current / j0)
else:
voltage = 0
return voltage
[docs]
def j0_gray(ni2, diffusivity, na, diffusion_length, arg, srv):
"""
Returns j0 (saturation current density in quasi-neutral regions of a solar cell)
as shown in eq. 3.128 in [1]_.
Parameters
----------
ni2 : numeric
intrinsic carrier concentration [m^-3].
diffusivity : numeric
carrier diffusivity [m/s].
na : numeric
doping density [m^-3].
diffusion_length : numeric
carrier diffusion length [m].
arg : numeric
(W-xn)/Lp term, (wafer_thickness-depletion region thickness)/diffusion_length [unitless].
srv : numeric
surface recombination velocity [m/s].
Returns
-------
numeric
j0 [A]
References
----------
.. [1] J. L. Gray, “The Physics of the Solar Cell,”
in Handbook of Photovoltaic Science and Engineering,
A. Luque and S. Hegedus, Eds. Chichester, UK: John Wiley & Sons, Ltd,
2011, pp. 82–129. doi: 10.1002/9780470974704.ch3.
"""
q = elementary_charge
prefactor = q * ni2 * diffusivity / (na * diffusion_length)
numerator = (diffusivity / diffusion_length) * np.sinh(arg) + srv * np.cosh(arg)
denominator = (diffusivity / diffusion_length) * np.cosh(arg) + srv * np.sinh(arg)
return prefactor * (numerator / denominator)
[docs]
def calc_voc_from_tau(tau, wafer_thickness, srv_rear, jsc, temperature, na=7.2e21):
"""
Return solar cell open-circuit voltage (Voc), given lifetime and other device parameters
Parameters
----------
tau : numeric
Carrier lifetime [us].
wafer_thickness : numeric
Wafer thickness [um].
srv_rear : numeric
Rear surface recombination velocity [cm/s].
jsc : numeric
Short-circuit current density [mA/cm^2].
temperature : numeric
Temperature [C].
na : numeric, default 7.2e21
Doping density [m^-3]. Default value corresponds to ~2 Ω-cm boron-doped c-Si.
Returns
-------
numeric
Device Voc [V].
"""
# unit conversions
tau = tau * 1e-6 # convert microseconds to seconds
wafer_thickness = wafer_thickness * 1e-6 # convert microns to meters
srv_rear = srv_rear / 100 # convert cm/s to m/s
jsc = jsc * 0.001 * 10000 # convert mA/cm^2 to A/m^2
temperature = convert_temperature(
temperature, "C", "K"
) # convert Celsius to Kelvin
return convert_i_to_v(tau, na, jsc, wafer_thickness, srv_rear, temperature)
[docs]
def calc_device_params(timesteps, cell_area=239):
"""
Returns device parameters given a Dataframe of Jsc and Voc
Parameters
----------
timesteps : DataFrame
Column names must include:
- ``'Jsc'``
- ``'Voc'``
cell_area : numeric, default 239
Cell area [cm^2]. 239 cm^2 is roughly the area of a 156x156mm pseudosquare "M0" wafer
Returns
-------
timesteps : DataFrame
Dataframe with new columns for Isc, FF, Pmp, and normalized Pmp
"""
timesteps.loc[:, "Isc"] = timesteps.loc[:, "Jsc"] * (cell_area / 1000)
timesteps.loc[:, "FF"] = ff_green(timesteps.loc[:, "Voc"])
timesteps.loc[:, "Pmp"] = (
timesteps.loc[:, "Voc"]
* timesteps.loc[:, "FF"]
* timesteps.loc[:, "Jsc"]
* (cell_area / 1000)
)
timesteps.loc[:, "Pmp_norm"] = timesteps.loc[:, "Pmp"] / timesteps.loc[0, "Pmp"]
return timesteps
[docs]
def calc_energy_loss(timesteps):
"""
Returns energy loss given a timeseries containing normalized changes in maximum power
Parameters
----------
timesteps : Dataframe
timesteps.index must be DatetimeIndex OR timesteps must include ``'Datetime'``
column with dtype datetime
Column names must include:
- ``'Pmp_norm'``, a column of normalized (0-1) maximum power such as returned by
Degradation.calc_device_params
Returns
-------
energy_loss : float
fractional energy loss over time
"""
if isinstance(timesteps.index, pd.DatetimeIndex):
start = timesteps.index[0]
timedelta = [(d - start).total_seconds() / 3600 for d in timesteps.index]
else:
start = timesteps["Datetime"].iloc[0]
timedelta = [(d - start).total_seconds() / 3600 for d in timesteps["Datetime"]]
pmp_norm = timesteps["Pmp_norm"]
energy_loss = 1 - (
simpson(pmp_norm, timedelta) / simpson(np.ones(len(pmp_norm)), timedelta)
)
return energy_loss
[docs]
def calc_regeneration_time(timesteps, x=80, rtol=1e-05):
"""
Returns time to x% regeneration, determined by the percentage of defects in State C.
Parameters
----------
timesteps : Dataframe
timesteps.index must be DatetimeIndex OR timesteps must include ``'Datetime'`` column
with dtype datetime
Column names must include:
- ``'NC'``, the percentage of defects in state C
x : numeric, default 80
percentage regeneration to look for. Note that 100% State C will take a very long time,
whereas in most cases >99% of power is regenerated after NC = ~80%
rel_tol : float, default = 1e-05
The relative tolerance parameter
Returns
-------
regen_time : timedelta
The time taken to reach x% regeneration
"""
if isinstance(timesteps.index, pd.DatetimeIndex):
start = timesteps.index[0]
stop_row = timesteps[np.isclose(timesteps["NC"], x, rtol)].iloc[0]
stop = stop_row.name
regen_time = stop - start
else:
start = timesteps["Datetime"].iloc[0]
stop_row = timesteps[np.isclose(timesteps["NC"], x, rtol)].iloc[0]
stop = stop_row["Datetime"]
regen_time = stop - start
return regen_time
[docs]
def calc_pmp_loss_from_tau_loss(
tau_0, tau_deg, cell_area, wafer_thickness, s_rear, generation=None, depth=None
):
"""
Function to estimate power loss from bulk lifetime loss
Parameters
----------
tau_0 : numeric
Initial bulk lifetime [us]
tau_deg : numeric
Degraded bulk lifetime [us]
cell_area : numeric
Cell area [cm^2]
wafer_thickness : numeric
Wafer thickness [um]
s_rear : numeric
Rear surface recombination velocity [cm/s]
Returns
-------
pmp_loss, pmp_0, pmp_deg : tuple of numeric
Power loss [%], Initial power [W], and Degraded power [W]
"""
if generation is None or depth is None:
path = os.path.join(DATA_DIR, "PVL_GenProfile.xlsx")
generation_df = pd.read_excel(path, header=0, engine="openpyxl")
generation = generation_df["Generation (cm-3s-1)"]
depth = generation_df["Depth (um)"]
jsc_0 = collection.calculate_jsc_from_tau_cp(
tau_0,
wafer_thickness=wafer_thickness,
d_base=27,
s_rear=s_rear,
generation=generation,
depth=depth,
)
jsc_deg = collection.calculate_jsc_from_tau_cp(
tau_deg,
wafer_thickness=wafer_thickness,
d_base=27,
s_rear=s_rear,
generation=generation,
depth=depth,
)
voc_0 = calc_voc_from_tau(
tau_0, wafer_thickness, s_rear, jsc_0, temperature=25, na=7.2e21
)
voc_deg = calc_voc_from_tau(
tau_deg, wafer_thickness, s_rear, jsc_deg, temperature=25, na=7.2e21
)
ff_0 = ff_green(voc_0)
ff_deg = ff_green(voc_deg)
pmp_0 = jsc_0 / 1000 * cell_area * voc_0 * ff_0
pmp_deg = jsc_deg / 1000 * cell_area * voc_deg * ff_deg
pmp_loss = (pmp_0 - pmp_deg) / pmp_0
return pmp_loss, pmp_0, pmp_deg
[docs]
def calc_ndd(tau_0, tau_deg):
"""
Calculates normalized defect density given starting and ending lifetimes
Parameters
----------
tau_0 : numeric
Initial bulk lifetime [us]
tau_deg : numeric
Degraded bulk lifetime [us]
Returns
-------
ndd : numeric
normalized defect density
"""
ndd = (1 / tau_deg) - (1 / tau_0)
return ndd
[docs]
def ff_green(voltage, temperature=298.15):
"""
Calculates the empirical expression for fill factor of Si cells from open-circuit voltage.
See [1]_, equation 4.
Parameters
----------
voltage : numeric
Open-circuit voltage of the solar cell [V].
temperature : numeric, default 298.15
Temperature of the solar cell [K].
Returns
-------
numeric
Fill factor of the solar cell
References
----------
.. [1] M. A. Green, “Solar cell fill factors: General graph and empirical expressions”,
Solid-State Electronics, vol. 24, pp. 788 - 789, 1981.
https://doi.org/10.1016/0038-1101(81)90062-9
"""
k = Boltzmann
q = elementary_charge
v = voltage * q / (k * temperature)
return (v - np.log(v + 0.72)) / (v + 1)
[docs]
def calc_injection_outdoors(results):
"""
Return "injection" of a pvlib modelchain cell/module/array operated at maximum power point.
Injection is normalized to "suns", the fraction of 1-sun irradiance.
Parameters
----------
results : a pvlib.ModelChainResult object having 'run_model'
Returns
-------
injection : numeric
"""
ee = results.effective_irradiance
injection = (
(results.dc["i_sc"] - results.dc["i_mp"]) / (results.dc["i_sc"]) * (ee / 1000)
)
# replace any too-small values with NaNs
injection = injection.mask(injection < 1e-5)
return injection
[docs]
def calc_letid_outdoors(
tau_0,
tau_deg,
wafer_thickness,
s_rear,
na_0,
nb_0,
nc_0,
weather_df,
meta,
mechanism_params,
generation_df=None,
d_base=27,
cell_area=243,
tilt=None,
azimuth=180,
module_parameters=None,
inverter_parameters=None,
temp_model="sapm",
temperature_model_parameters="open_rack_glass_polymer",
):
"""
Models outdoor LETID progression of a device.
Parameters
----------
tau_0 : numeric
Initial bulk lifetime [us]
tau_deg : numeric
Fully degraded bulk lifetime [us]
wafer_thickness : numeric
Wafer thickness [um]
s_rear : numeric
Rear surface recombination velocity [cm/s]
na_0 : numeric
Initial percentage of defects in state A [%]
nb_0 : numeric
Initial percentage of defects in state B [%]
nc_0 : numeric
Initial percentage of defects in state C [%]
weather_df : pandas DataFrame
Makes use of pvlib ModelChain.run_model. Similar to pvlib, column names MUST include:
- ``'dni'``
- ``'ghi'``
- ``'dhi'``
Optional columns are:
- ``'temp_air'``
- ``'cell_temperature'``
- ``'module_temperature'``
- ``'wind_speed'``
- ``'albedo'``
meta : dict
dict of location information for builidng a pvlib.Location object, e.g. from psm3 data
accessed via pvlib.iotools.read_psm3
mechanism_params : str
Name for mechanism parameters set. Parameters are coded in 'kinetic_parameters.json'.
These are typically taken from literature studies of transtions in the 3-state model.
They allow for calculation the excess carrier density of literature experiments (dn_lit)
generation_df : pandas DataFrame or None
Dataframe of an optical generation profile for a solar cell used to calculate current
collection. If None, loads default generation profile from 'PVL_GenProfile.xlsx'.
If not None, column names must include:
- ``'Generation (cm-3s-1)'``
- ``'Depth (um)'``
TODO: improve this.
d_base : numeric, default 27
Minority carrier diffusivity of the base of the solar cell [cm^2/Vs].
cell_area : numeric, default 239
Cell area [cm^2]. 239 cm^2 is roughly the area of a 156x156mm pseudosquare "M0" wafer
tilt : numeric or None, default None
Tilt angle of system. If None, defaults to location latitude
azimuth : numeric, default 180
Azimuth angle of the syste. Default is 180, i.e., south-facing.
module_parameters : dict or None, default None
pvlib module parameters. see pvlib documentation for details. Note that this model requires
full DC power results, so requires either the CEC or SAPM model, (i.e., not PVWatts).
If None, defaults to "Jinko_Solar_Co___Ltd_JKM260P_60" from the CEC module database.
inverter_parameters : dict or None, default None
pvlib inverter parameters. see pvlib documentation for details. .
temp_model : str, default "sapm"
pvlib temperature model, either "sapm" or "pvsyst". See pvlib.temperature.
temperature_model_parameters : str, default "open_rack_glass_polymer"
Temperature model parameters as required by the selected model in pvlib.temperature
Returns
-------
timesteps : pandas DataFrame
Datafame containing defect state percentages, lifetime, and device electrical parameters
See also
--------
pvlib.modelchain.ModelChain.run_model
pvlib.iotools.read_psm3
pvlib.pvsystem.PVSystem
pvlib.temperature
"""
# Set up system, run pvlib.modelchain, and get the results we need: cell temp and injection
lat = float(meta["latitude"])
lon = float(meta["longitude"])
tz = meta["tz"]
elevation = meta["altitude"]
if tilt is None:
surface_tilt = lat # latitude tilt
else:
surface_tilt = tilt
surface_azimuth = azimuth
if module_parameters is None:
cec_modules = pvlib.pvsystem.retrieve_sam("CECMod")
module_parameters = cec_modules[
"Jinko_Solar_Co___Ltd_JKM260P_60"
] # a random module from the CEC database
location = pvlib.location.Location(lat, lon, tz, elevation)
if inverter_parameters is None:
inverter_parameters = {
"pdc0": 1000
} # inverter parameters are hard-coded, because we don't care about AC results
system = pvlib.pvsystem.PVSystem(
surface_tilt=surface_tilt,
surface_azimuth=surface_azimuth,
module_parameters=module_parameters,
inverter_parameters=inverter_parameters,
temperature_model_parameters=pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[
temp_model
][temperature_model_parameters],
)
mc = pvlib.modelchain.ModelChain(
system, location, aoi_model="physical", spectral_model="no_loss"
)
mc.run_model(weather_df)
injection = calc_injection_outdoors(mc.results) # get injection from DC results
temperature = mc.results.cell_temperature
# Set up timesteps to loop through
timesteps = pd.DataFrame(
{"Temperature": temperature, "Injection": injection}
) # create a DataFrame with cell temperature and injection
timesteps.reset_index(inplace=True) # reset the index so datetime is a column.
timesteps.rename(columns={"index": "time"}, inplace=True)
timesteps.reset_index(inplace=True, drop=True)
# create columns for defect state percentages and lifetime, fill with NaNs for now, to fill iteratively below
timesteps[["NA", "NB", "NC", "tau"]] = np.nan
# assign first timestep defect state percentages
timesteps.loc[0, ["NA", "NB", "NC"]] = na_0, nb_0, nc_0
# calculate tau for the first timestep
timesteps.loc[0, "tau"] = tau_now(tau_0, tau_deg, nb_0)
if generation_df is None:
generation_df = pd.read_excel(
os.path.join(DATA_DIR, "PVL_GenProfile.xlsx"), header=0
) # this is an optical generation profile generated by PVLighthouse's OPAL2 default model for 1-sun, normal incident AM1.5 sunlight on a 180-um thick SiNx-coated, pyramid-textured wafer.
generation = generation_df["Generation (cm-3s-1)"]
depth = generation_df["Depth (um)"]
else:
generation = generation_df[
"Generation (cm-3s-1)"
] # TODO: fix this to accept multiple formats of generation depth profile
depth = generation_df["Depth (um)"]
mechanism_params = utilities.get_kinetics(mechanism_params)
for index, timestep in timesteps.iterrows():
# first row tau has already been assigned
if index == 0:
# calc device parameters for first row
tau = tau_0
jsc = collection.calculate_jsc_from_tau_cp(
tau, wafer_thickness, d_base, s_rear, generation, depth
)
voc = calc_voc_from_tau(tau, wafer_thickness, s_rear, jsc, temperature=25)
timesteps.at[index, "Jsc"] = jsc
timesteps.at[index, "Voc"] = voc
elif timestep["Injection"] == 0:
pass # TODO, skip rows where injeciton is 0, because these won't induce letid.
# TODO this is where dark letid will need to be fixed.
# loop through rows, new tau calculated based on previous NB. Reaction proceeds based on new tau.
else:
n_A = timesteps.at[index - 1, "NA"]
n_B = timesteps.at[index - 1, "NB"]
n_C = timesteps.at[index - 1, "NC"]
tau = tau_now(tau_0, tau_deg, n_B)
jsc = collection.calculate_jsc_from_tau_cp(
tau, wafer_thickness, d_base, s_rear, generation, depth
)
temperature = timesteps.at[index, "Temperature"]
injection = timesteps.at[index, "Injection"]
# calculate defect reaction kinetics: reaction constant and carrier concentration factor.
k_AB = k_ij(
mechanism_params["v_ab"], mechanism_params["ea_ab"], temperature
)
k_BA = k_ij(
mechanism_params["v_ba"], mechanism_params["ea_ba"], temperature
)
k_BC = k_ij(
mechanism_params["v_bc"], mechanism_params["ea_bc"], temperature
)
k_CB = k_ij(
mechanism_params["v_cb"], mechanism_params["ea_cb"], temperature
)
x_ab = carrier_factor(
tau,
"ab",
temperature,
injection,
jsc,
wafer_thickness,
s_rear,
mechanism_params,
)
x_ba = carrier_factor(
tau,
"ba",
temperature,
injection,
jsc,
wafer_thickness,
s_rear,
mechanism_params,
)
x_bc = carrier_factor(
tau,
"bc",
temperature,
injection,
jsc,
wafer_thickness,
s_rear,
mechanism_params,
)
# calculate the instantaneous change in NA, NB, and NC
dN_Adt = (k_BA * n_B * x_ba) - (k_AB * n_A * x_ab)
dN_Bdt = (
(k_AB * n_A * x_ab) + (k_CB * n_C) - ((k_BA * x_ba + k_BC * x_bc) * n_B)
)
dN_Cdt = (k_BC * n_B * x_bc) - (k_CB * n_C)
t_step = (
timesteps.at[index, "time"] - timesteps.at[index - 1, "time"]
).total_seconds()
# assign new defect state percentages
timesteps.at[index, "NA"] = n_A + dN_Adt * t_step
timesteps.at[index, "NB"] = n_B + dN_Bdt * t_step
timesteps.at[index, "NC"] = n_C + dN_Cdt * t_step
# calculate device parameters
timesteps.at[index, "Jsc"] = jsc
timesteps.at[index, "Voc"] = calc_voc_from_tau(
tau, wafer_thickness, s_rear, jsc, temperature=25
)
timesteps["tau"] = tau_now(tau_0, tau_deg, timesteps["NB"])
timesteps = calc_device_params(timesteps, cell_area)
timesteps.set_index("time", inplace=True)
return timesteps
[docs]
def calc_letid_lab(
tau_0,
tau_deg,
wafer_thickness,
s_rear,
na_0,
nb_0,
nc_0,
injection,
temperature,
mechanism_params,
duration="3W",
freq="min",
start=None,
generation_df=None,
d_base=27,
cell_area=239,
):
"""
Models LETID progression in a constant temperature and injection (i.e. lab-based accelerated
test) environment.
Parameters
----------
tau_0 : numeric
Initial bulk lifetime [us]
tau_deg : numeric
Fully degraded bulk lifetime [us]
wafer_thickness : numeric
Wafer thickness [um]
s_rear : numeric
Rear surface recombination velocity [cm/s]
na_0 : numeric
Initial percentage of defects in state A [%]
nb_0 : numeric
Initial percentage of defects in state B [%]
nc_0 : numeric
Initial percentage of defects in state C [%]
injection : float
Injection of device. Normalized to 1-sun illumnation or short circuit current.
Typical injection in standard accelerated testing is 2x(Isc-Imp), i.e., roughly 0.1.
TODO: accept timeseries of injection for modeling variable-condition testing.
temperature : numeric
Test temperature of device [C]. IEC TS 63342 specifies 75C.
mechanism_params : str
Name for mechanism parameters set. Parameters are coded in 'kinetic_parameters.json'.
These are typically taken from literature studies of transtions in the 3-state model.
They allow for calculation the excess carrier density of literature experiments (dn_lit)
duration : str, default "3W"
Duration of modeled test. Generates a timeseries using pandas.to_timedelta. Default is 3
weeks, i.e. the length of IEC TS 63342.
freq : str, default "min"
See pandas.date_range for details. In general, choose short time intervals unless you're
sure defect reactions are proceeding very slowly.
start : str or datetime-like or None, default None
If provided, defines the start time of the test. If none, defaults to now.
generation_df : pandas DataFrame or None
Dataframe of an optical generation profile for a solar cell used to calculate current
collection. If None, loads default generation profile from 'PVL_GenProfile.xlsx'.
If not None, column names must include:
- ``'Generation (cm-3s-1)'``
- ``'Depth (um)'``
TODO: improve this.
d_base : numeric, default 27
Minority carrier diffusivity of the base of the solar cell [cm^2/Vs].
cell_area : numeric, default 239
Cell area [cm^2]. 239 cm^2 is roughly the area of a 156x156mm pseudosquare "M0" wafer
Returns
-------
timesteps : pandas DataFrame
Datafame containing defect state percentages, lifetime, and device electrical parameters
"""
if start is None:
start = datetime.datetime.now()
# constant temperature and injection
if (
isinstance(injection, int)
or isinstance(injection, float)
and isinstance(temperature, int)
or isinstance(temperature, float)
):
# default is 3 weeks of 1-minute interval timesteps. In general, we should select small timesteps unless we are sure defect reactions are proceeding very slowly
timesteps = pd.date_range(
start, end=pd.to_datetime(start) + pd.to_timedelta(duration), freq=freq
)
timesteps = pd.DataFrame(timesteps, columns=["Datetime"])
temperature = np.full(len(timesteps), temperature)
injection = np.full(len(timesteps), injection)
timesteps["Temperature"] = temperature
timesteps["Injection"] = injection
# create columns for defect state percentages and lifetime, fill with NaNs for now, to fill iteratively below
timesteps[["NA", "NB", "NC", "tau"]] = np.nan
# assign first timestep defect state percentages
timesteps.loc[0, ["NA", "NB", "NC"]] = na_0, nb_0, nc_0
# calculate tau for the first timestep
timesteps.loc[0, "tau"] = tau_now(tau_0, tau_deg, nb_0)
# TODO: user-defined injection and temperature profiles
# elif len(injection) > 1 and len(injection)==len(temperature):
# pd.merge(injection,temperature)
else:
print("can only define constant temp and injection for now")
if generation_df is None:
generation_df = pd.read_excel(
os.path.join(DATA_DIR, "PVL_GenProfile.xlsx"), header=0
) # this is an optical generation profile generated by PVLighthouse's OPAL2 default model for 1-sun, normal incident AM1.5 sunlight on a 180-um thick SiNx-coated, pyramid-textured wafer.
generation = generation_df["Generation (cm-3s-1)"]
depth = generation_df["Depth (um)"]
else:
generation = generation_df[
"Generation (cm-3s-1)"
] # TODO: fix this to accept multiple formats of generation depth profile
depth = generation_df["Depth (um)"]
mechanism_params = utilities.get_kinetics(mechanism_params)
for index, timestep in timesteps.iterrows():
# first row tau has already been assigned
if index == 0:
# calc device parameters for first row
tau = tau_0
jsc = collection.calculate_jsc_from_tau_cp(
tau, wafer_thickness, d_base, s_rear, generation, depth
)
voc = calc_voc_from_tau(tau, wafer_thickness, s_rear, jsc, temperature=25)
timesteps.at[index, "Jsc"] = jsc
timesteps.at[index, "Voc"] = voc
elif timestep["Injection"] == 0:
pass # TODO, skip rows where injeciton is 0, because these won't induce letid.
# TODO this is where dark letid will need to be fixed.
# loop through rows, new tau calculated based on previous NB. Reaction proceeds based on new tau.
else:
n_A = timesteps.at[index - 1, "NA"]
n_B = timesteps.at[index - 1, "NB"]
n_C = timesteps.at[index - 1, "NC"]
tau = tau_now(tau_0, tau_deg, n_B)
jsc = collection.calculate_jsc_from_tau_cp(
tau, wafer_thickness, d_base, s_rear, generation, depth
)
temperature = timesteps.at[index, "Temperature"]
injection = timesteps.at[index, "Injection"]
# calculate defect reaction kinetics: reaction constant and carrier concentration factor.
k_AB = k_ij(
mechanism_params["v_ab"], mechanism_params["ea_ab"], temperature
)
k_BA = k_ij(
mechanism_params["v_ba"], mechanism_params["ea_ba"], temperature
)
k_BC = k_ij(
mechanism_params["v_bc"], mechanism_params["ea_bc"], temperature
)
k_CB = k_ij(
mechanism_params["v_cb"], mechanism_params["ea_cb"], temperature
)
x_ab = carrier_factor(
tau,
"ab",
temperature,
injection,
jsc,
wafer_thickness,
s_rear,
mechanism_params,
)
x_ba = carrier_factor(
tau,
"ba",
temperature,
injection,
jsc,
wafer_thickness,
s_rear,
mechanism_params,
)
x_bc = carrier_factor(
tau,
"bc",
temperature,
injection,
jsc,
wafer_thickness,
s_rear,
mechanism_params,
)
# calculate the instantaneous change in NA, NB, and NC
dN_Adt = (k_BA * n_B * x_ba) - (k_AB * n_A * x_ab)
dN_Bdt = (
(k_AB * n_A * x_ab) + (k_CB * n_C) - ((k_BA * x_ba + k_BC * x_bc) * n_B)
)
dN_Cdt = (k_BC * n_B * x_bc) - (k_CB * n_C)
t_step = (
timesteps.at[index, "Datetime"] - timesteps.at[index - 1, "Datetime"]
).total_seconds()
# assign new defect state percentages
timesteps.at[index, "NA"] = n_A + dN_Adt * t_step
timesteps.at[index, "NB"] = n_B + dN_Bdt * t_step
timesteps.at[index, "NC"] = n_C + dN_Cdt * t_step
# calculate device parameters
timesteps.at[index, "Jsc"] = jsc
timesteps.at[index, "Voc"] = calc_voc_from_tau(
tau, wafer_thickness, s_rear, jsc, temperature=25
)
timesteps["tau"] = tau_now(tau_0, tau_deg, timesteps["NB"])
timesteps = calc_device_params(timesteps, cell_area)
return timesteps