solphin 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solphin/__init__.py +9 -0
- solphin/db_fom.py +304 -0
- solphin/db_plotting.py +81 -0
- solphin/final_results.py +73 -0
- solphin/optics.py +220 -0
- solphin/pv_fom.py +494 -0
- solphin/spectral.py +138 -0
- solphin/vasp_inputs.py +232 -0
- solphin/version.py +1 -0
- solphin-0.0.0.dist-info/METADATA +30 -0
- solphin-0.0.0.dist-info/RECORD +14 -0
- solphin-0.0.0.dist-info/WHEEL +5 -0
- solphin-0.0.0.dist-info/licenses/LICENSE +21 -0
- solphin-0.0.0.dist-info/top_level.txt +1 -0
solphin/__init__.py
ADDED
solphin/db_fom.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import scipy.constants as sc
|
|
2
|
+
import numpy as np
|
|
3
|
+
import logging
|
|
4
|
+
from importlib.resources import files
|
|
5
|
+
|
|
6
|
+
logging.basicConfig(level=logging.INFO)
|
|
7
|
+
|
|
8
|
+
''' This section details the calculation of the detailed balance limit efficiency and associated values.'''
|
|
9
|
+
|
|
10
|
+
h = sc.h # Planck's constant (J·s)
|
|
11
|
+
c = sc.c # Speed of light (m/s)
|
|
12
|
+
k = sc.k # Boltzmann constant (J/K)
|
|
13
|
+
q = sc.e # Elementary charge (Coulombs)
|
|
14
|
+
|
|
15
|
+
# Convert the spectrum to the useful units - taken from https://github.com/kaklin/sq-limit?tab=readme-ov-file
|
|
16
|
+
|
|
17
|
+
def load_spectrum(spectrum_type):
|
|
18
|
+
|
|
19
|
+
if spectrum_type == "AM1.5":
|
|
20
|
+
filename = 'ASTMG173.csv'
|
|
21
|
+
|
|
22
|
+
elif spectrum_type == "Fluorescent":
|
|
23
|
+
filename = 'fluorescent.csv'
|
|
24
|
+
|
|
25
|
+
elif spectrum_type == "Blue LED":
|
|
26
|
+
filename = 'led_blue.csv'
|
|
27
|
+
|
|
28
|
+
elif spectrum_type == "Green LED":
|
|
29
|
+
filename = 'led_green.csv'
|
|
30
|
+
|
|
31
|
+
elif spectrum_type == "Red LED":
|
|
32
|
+
filename = 'led_red.csv'
|
|
33
|
+
|
|
34
|
+
elif spectrum_type == "White LED":
|
|
35
|
+
filename = 'led_white.csv'
|
|
36
|
+
|
|
37
|
+
elif spectrum_type == "IR LED":
|
|
38
|
+
filename = 'led_ir.csv'
|
|
39
|
+
|
|
40
|
+
elif spectrum_type == "Photopic":
|
|
41
|
+
filename = 'photopic.csv'
|
|
42
|
+
|
|
43
|
+
else:
|
|
44
|
+
print("Unrecognisable spectrum selected")
|
|
45
|
+
print("Options: AM1.5, Fluorescent, Blue LED, Green LED, Red LED, White LED, IR LED, Photopic")
|
|
46
|
+
print("reverting to AM1.5")
|
|
47
|
+
|
|
48
|
+
filename = 'ASTMG173.csv'
|
|
49
|
+
|
|
50
|
+
csv_path = files("placeholder.resources") / f"{filename}"
|
|
51
|
+
|
|
52
|
+
with csv_path.open("r", encoding="utf-8") as f:
|
|
53
|
+
spectrum = np.loadtxt(f, delimiter=",", skiprows=1)
|
|
54
|
+
|
|
55
|
+
return spectrum
|
|
56
|
+
|
|
57
|
+
def convert_spectrum(spectrum):
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
Converts the input spectrum from standard format to the required units for this code.
|
|
61
|
+
|
|
62
|
+
Parameters:
|
|
63
|
+
spectrum(numpy.ndarray): Input spectrum loaded from a csv file with numpy.loadtxt.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
67
|
+
|
|
68
|
+
Spectrum input:
|
|
69
|
+
y: Irradiance (W/m2/nm)
|
|
70
|
+
x: Wavelength (nm)
|
|
71
|
+
Converted output:
|
|
72
|
+
y: Number of photons (Np/m2/s/dE)
|
|
73
|
+
x: Energy (eV)
|
|
74
|
+
"""
|
|
75
|
+
converted = np.copy(spectrum)
|
|
76
|
+
converted[:, 0] = converted[:, 0] * 1e-9 # wavelength to m
|
|
77
|
+
converted[:, 1] = converted[:, 1] / 1e-9 # irradiance to W/m2/m (from W/m2/nm)
|
|
78
|
+
|
|
79
|
+
E = h * c / converted[:, 0]
|
|
80
|
+
d_lambda_d_E = h * c / E**2
|
|
81
|
+
converted[:, 1] = converted[:, 1] * d_lambda_d_E * q / E
|
|
82
|
+
converted[:, 0] = E / q
|
|
83
|
+
|
|
84
|
+
return converted
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def photons_above_bandgap(E_gap, photon_spectrum):
|
|
88
|
+
"""Counts number of photons above given bandgap.
|
|
89
|
+
|
|
90
|
+
Parameters:
|
|
91
|
+
E_gap(float): Optical Band Gap in eV
|
|
92
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
(float): Integration of the spectrum for the number of photons above the bandgap.
|
|
96
|
+
"""
|
|
97
|
+
indexes = np.where(photon_spectrum[:, 0] > E_gap)
|
|
98
|
+
y = photon_spectrum[indexes, 1][0]
|
|
99
|
+
x = photon_spectrum[indexes, 0][0]
|
|
100
|
+
return np.trapz(y[::-1], x[::-1])
|
|
101
|
+
|
|
102
|
+
def rr0(E_gap, photon_spectrum, Tcell):
|
|
103
|
+
'''
|
|
104
|
+
Calculates the radiative recombination rate at 0 Quasi-Fermi Level splitting.
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
E_gap(float): Optical Band Gap in eV
|
|
108
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
109
|
+
Tcell(float): Operating temperature of the cell in K
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Radiative recomination rate(float) in cm⁻³s⁻¹
|
|
113
|
+
|
|
114
|
+
'''
|
|
115
|
+
k_eV = k / q
|
|
116
|
+
h_eV = h / q
|
|
117
|
+
const = (2 * np.pi) / (c**2 * h_eV**3)
|
|
118
|
+
|
|
119
|
+
k_eV = k / q
|
|
120
|
+
E = photon_spectrum[::-1, ] # in increasing order of bandgap energy
|
|
121
|
+
egap_index = np.where(E[:, 0] >= E_gap)
|
|
122
|
+
numerator = E[:, 0]**2
|
|
123
|
+
exponential_in = E[:, 0] / (k_eV * Tcell)
|
|
124
|
+
denominator = np.exp(exponential_in) - 1
|
|
125
|
+
integrand = numerator / denominator
|
|
126
|
+
|
|
127
|
+
integral = np.trapz(integrand[egap_index], E[egap_index, 0])
|
|
128
|
+
|
|
129
|
+
result = const * integral
|
|
130
|
+
return result[0]
|
|
131
|
+
|
|
132
|
+
def recomb_rate(E_gap, photon_spectrum, voltage, Tcell):
|
|
133
|
+
'''
|
|
134
|
+
Calculates the radiative recombination rate.
|
|
135
|
+
|
|
136
|
+
Parameters:
|
|
137
|
+
E_gap(float): Optical Band Gap in eV
|
|
138
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
139
|
+
voltage(float): Open circuit voltage in V
|
|
140
|
+
Tcell(float): Operating temperature of the cell in K
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Radiative recomination rate(float) in cm⁻³s⁻¹
|
|
144
|
+
|
|
145
|
+
'''
|
|
146
|
+
|
|
147
|
+
print ('recomb rate')
|
|
148
|
+
return q * rr0(E_gap, photon_spectrum) * np.exp(q * voltage / (k * Tcell))
|
|
149
|
+
|
|
150
|
+
def current_density(E_gap, photon_spectrum, voltage, Tcell):
|
|
151
|
+
'''
|
|
152
|
+
Calculates the current density.
|
|
153
|
+
|
|
154
|
+
Parameters:
|
|
155
|
+
E_gap(float): Optical Band Gap in eV
|
|
156
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
157
|
+
voltage(float): Open circuit voltage in V
|
|
158
|
+
Tcell(float): Operating temperature of the cell in K
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Current density (float): Current that flows across a cross sectional area in C cm⁻³s⁻¹.
|
|
162
|
+
|
|
163
|
+
'''
|
|
164
|
+
|
|
165
|
+
return q * (photons_above_bandgap(E_gap, photon_spectrum) - rr0(E_gap, photon_spectrum, Tcell) * np.exp(q * voltage / (k * Tcell)))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def jsc(E_gap, photon_spectrum, Tcell):
|
|
169
|
+
|
|
170
|
+
'''
|
|
171
|
+
Calculates the current density.
|
|
172
|
+
|
|
173
|
+
Parameters:
|
|
174
|
+
E_gap(float): Optical Band Gap in eV
|
|
175
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Short circuit current density (float): Current that flows across a cross sectional area at 0 applied voltage in C cm⁻³s⁻¹.
|
|
179
|
+
|
|
180
|
+
'''
|
|
181
|
+
|
|
182
|
+
return current_density(E_gap, photon_spectrum, 0, Tcell)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def voc(E_gap, photon_spectrum, Tcell):
|
|
186
|
+
'''
|
|
187
|
+
Calculates the open circuit voltage.
|
|
188
|
+
|
|
189
|
+
Parameters:
|
|
190
|
+
E_gap(float): Optical Band Gap in eV
|
|
191
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
192
|
+
Tcell(float): Operating temperature of the cell in K
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Open circuit voltage (float): Maximum voltage across a solar cell with no current flow in V.
|
|
196
|
+
|
|
197
|
+
'''
|
|
198
|
+
|
|
199
|
+
# print 'voc'
|
|
200
|
+
return (k * Tcell / q) * np.log(photons_above_bandgap(E_gap, photon_spectrum) / rr0(E_gap, photon_spectrum, Tcell))
|
|
201
|
+
|
|
202
|
+
def v_at_mpp(E_gap, photon_spectrum):
|
|
203
|
+
|
|
204
|
+
'''
|
|
205
|
+
Calculates the voltage at maximum power point (mpp) of a solar cell.
|
|
206
|
+
|
|
207
|
+
Parameters:
|
|
208
|
+
E_gap(float): Optical Band Gap in eV
|
|
209
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Voltage at MPP (float): Voltage across a solar cell at the maximum power point in V.
|
|
213
|
+
|
|
214
|
+
'''
|
|
215
|
+
|
|
216
|
+
v_open = voc(E_gap, photon_spectrum)
|
|
217
|
+
# print v_open
|
|
218
|
+
v = np.linspace(0, v_open)
|
|
219
|
+
index = np.where(v * current_density(E_gap, photon_spectrum, v)==max(v * current_density(E_gap, photon_spectrum, v)))
|
|
220
|
+
return v[index][0]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def j_at_mpp(E_gap, photon_spectrum):
|
|
224
|
+
|
|
225
|
+
'''
|
|
226
|
+
Calculates the current at maximum power point (mpp) of a solar cell.
|
|
227
|
+
|
|
228
|
+
Parameters:
|
|
229
|
+
E_gap(float): Optical Band Gap in eV
|
|
230
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Current at MPP (float): Current across a solar cell at the maximum power point.
|
|
234
|
+
|
|
235
|
+
'''
|
|
236
|
+
|
|
237
|
+
return max_power(E_gap, photon_spectrum) / v_at_mpp(E_gap, photon_spectrum)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def max_power(E_gap, photon_spectrum, Tcell):
|
|
241
|
+
|
|
242
|
+
'''
|
|
243
|
+
Calculates the maximum power of a solar cell.
|
|
244
|
+
|
|
245
|
+
Parameters:
|
|
246
|
+
E_gap(float): Optical Band Gap in eV
|
|
247
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
248
|
+
Tcell(float): Operating temperature of the cell in K
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Maximum power (float): Maximum power of the solar cell in V C cm⁻³ s⁻¹.
|
|
252
|
+
|
|
253
|
+
'''
|
|
254
|
+
|
|
255
|
+
v_open = voc(E_gap, photon_spectrum, Tcell)
|
|
256
|
+
v = np.linspace(0, v_open)
|
|
257
|
+
index = np.where(v * current_density(E_gap, photon_spectrum, v, Tcell)==max(v * current_density(E_gap, photon_spectrum, v, Tcell)))
|
|
258
|
+
return max(v * current_density(E_gap, photon_spectrum, v, Tcell))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def max_eff(E_gap, photon_spectrum, Tcell):
|
|
262
|
+
|
|
263
|
+
'''
|
|
264
|
+
Calculates the maximum efficiency of a solar cell.
|
|
265
|
+
|
|
266
|
+
Parameters:
|
|
267
|
+
E_gap(float): Optical Band Gap in eV
|
|
268
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
269
|
+
Tcell(float): Operating temperature of the cell in K
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Maximum efficiency (float): Maximum effeciency of the solar cell relative to the total irradiance in %.
|
|
273
|
+
'''
|
|
274
|
+
|
|
275
|
+
photon_spectrum_1 = photon_spectrum[::-1, 1]
|
|
276
|
+
photon_spectrum_0 = photon_spectrum[::-1, 0]
|
|
277
|
+
|
|
278
|
+
irradiance = np.trapz(photon_spectrum_1 * q * photon_spectrum_0, photon_spectrum_0)
|
|
279
|
+
return max_power(E_gap, photon_spectrum, Tcell) / irradiance
|
|
280
|
+
|
|
281
|
+
def FillFactor(E_gap, photon_spectrum, Tcell):
|
|
282
|
+
|
|
283
|
+
'''
|
|
284
|
+
Calculates the fill factor of a solar cell.
|
|
285
|
+
|
|
286
|
+
Parameters:
|
|
287
|
+
E_gap(float): Optical Band Gap in eV
|
|
288
|
+
photon_spectrum(numpy.ndarray): Output spectrum as numpy ndarray.
|
|
289
|
+
Tcell(float): Operating temperature of the cell in K
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
fill_factor (float): The fill factor of a solar cell.
|
|
293
|
+
'''
|
|
294
|
+
|
|
295
|
+
j_sc = jsc(E_gap, photon_spectrum)
|
|
296
|
+
v_oc = voc(E_gap, photon_spectrum, Tcell)
|
|
297
|
+
v_mpp = v_at_mpp(E_gap, photon_spectrum)
|
|
298
|
+
j_mpp = j_at_mpp(E_gap, photon_spectrum)
|
|
299
|
+
|
|
300
|
+
fill_factor = (j_mpp * v_mpp) / (j_sc, v_oc)
|
|
301
|
+
|
|
302
|
+
return fill_factor
|
|
303
|
+
|
|
304
|
+
|
solphin/db_plotting.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import solphin.db_fom as db_fom
|
|
2
|
+
import numpy as np
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
logging.basicConfig(level=logging.INFO)
|
|
8
|
+
|
|
9
|
+
def photons_above_bandgap_plot(spectrum, Egap):
|
|
10
|
+
"""Plot of photons above bandgap as a function of bandgap"""
|
|
11
|
+
a = np.copy(spectrum)
|
|
12
|
+
for row in a:
|
|
13
|
+
# print row
|
|
14
|
+
row[1] = db_fom.photons_above_bandgap(row[0], spectrum)
|
|
15
|
+
plt.plot(a[:, 0], a[:, 1], color='#231123')
|
|
16
|
+
|
|
17
|
+
p_above_1_1 = db_fom.photons_above_bandgap(Egap, spectrum)
|
|
18
|
+
plt.plot([Egap], [p_above_1_1], 'ro', color='#FF6666')
|
|
19
|
+
plt.text(Egap+0.05, p_above_1_1, '{}eV, {:.4}'.format(Egap, p_above_1_1))
|
|
20
|
+
|
|
21
|
+
plt.xlabel('$E_{gap}$ (eV)')
|
|
22
|
+
plt.ylabel('# Photons $m^{-2}s^{-1}$')
|
|
23
|
+
plt.title('Number of above-bandgap \nphotons as a function of bandgap')
|
|
24
|
+
plt.show()
|
|
25
|
+
|
|
26
|
+
def iv_curve_plot(egap, spectrum, Tcell, power=False):
|
|
27
|
+
"""Plots the ideal IV curve, or the ideal power for a given material"""
|
|
28
|
+
v_open = db_fom.voc(egap, spectrum, Tcell)
|
|
29
|
+
v = np.linspace(0, v_open)
|
|
30
|
+
if power:
|
|
31
|
+
p = v * db_fom.current_density(egap, spectrum, v, Tcell)
|
|
32
|
+
plt.xlabel('Voltage (V)')
|
|
33
|
+
plt.ylabel('Power generated ($W$)')
|
|
34
|
+
plt.title('Power Curve')
|
|
35
|
+
plt.plot(v, p, color='#231123')
|
|
36
|
+
else:
|
|
37
|
+
i = db_fom.current_density(egap, spectrum, v, Tcell)
|
|
38
|
+
plt.xlabel('Voltage (V)')
|
|
39
|
+
plt.ylabel('Current density $J$ ($Am^{-2}$)')
|
|
40
|
+
plt.title('IV Curve')
|
|
41
|
+
plt.plot(v, i, color='#231123')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def iv_curve_plot_2(egap, spectrum, Tcell, power=False):
|
|
45
|
+
"""Plots the ideal IV curve, and the ideal power for a given material"""
|
|
46
|
+
v_open = db_fom.voc(egap, spectrum, Tcell)
|
|
47
|
+
v = np.linspace(0, v_open)
|
|
48
|
+
|
|
49
|
+
fig, ax1 = plt.subplots()
|
|
50
|
+
p = v * db_fom.current_density(egap, spectrum, v, Tcell)
|
|
51
|
+
i = db_fom.current_density(egap, spectrum, v, Tcell)
|
|
52
|
+
|
|
53
|
+
ax1.plot(v, i, color='#231123')
|
|
54
|
+
ax1.set_xlabel('Voltage (V)')
|
|
55
|
+
ax1.set_ylabel('Current density $J$ ($Am^{-2}$)')
|
|
56
|
+
ax1.legend(['Current'], loc=2)
|
|
57
|
+
|
|
58
|
+
ax2 = ax1.twinx()
|
|
59
|
+
ax2.plot(v, p, color='#FF6666')
|
|
60
|
+
ax2.set_ylabel('Power generated ($W$)')
|
|
61
|
+
ax2.legend(['Power'], loc=3)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def sq_limit_plot(spectrum, Egap, Tcell):
|
|
66
|
+
# Plot the famous SQ limit
|
|
67
|
+
a = np.copy(spectrum)
|
|
68
|
+
# Not for whole array hack to remove divide by 0 errors
|
|
69
|
+
for row in a[2:]:
|
|
70
|
+
# print row
|
|
71
|
+
row[1] = db_fom.max_eff(row[0], spectrum, Tcell)
|
|
72
|
+
# Not plotting whole array becase some bad values happen
|
|
73
|
+
plt.plot(a[2:, 0], a[2:, 1])
|
|
74
|
+
e_gap = Egap
|
|
75
|
+
p_above_1_1 = db_fom.max_eff(e_gap, spectrum, Tcell)
|
|
76
|
+
plt.plot([e_gap], [p_above_1_1], 'ro')
|
|
77
|
+
plt.text(e_gap+0.05, p_above_1_1, '{}eV, {:.4}'.format(e_gap, p_above_1_1))
|
|
78
|
+
|
|
79
|
+
plt.xlabel('$E_{gap}$ (eV)')
|
|
80
|
+
plt.ylabel('Max efficiency')
|
|
81
|
+
plt.title('SQ Limit')
|
solphin/final_results.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from solphin.pv_fom import Final_equation
|
|
2
|
+
from solphin.db_fom import max_eff
|
|
3
|
+
|
|
4
|
+
# Calculating equation 33 from the FOM paper
|
|
5
|
+
|
|
6
|
+
def Crovetto_efficiency(E_gap, photon_spectrum, alpha, tau, sigma, dos_mass, dop_density, epsilon, mu, Tcell):
|
|
7
|
+
|
|
8
|
+
''' Calculates the final value for the photovoltaic figure of merit from Crovetto 2024
|
|
9
|
+
|
|
10
|
+
Parameters:
|
|
11
|
+
E_gap(float): Optical Band Gap in eV
|
|
12
|
+
photon_spectrum(numpy.ndarrray): Converted input spectrum from DB_FOM.convert_spectrum y: Number of photons (Np/m2/s/dE) x: Energy (eV)
|
|
13
|
+
alpha(float): Spectral average of incident light in cm⁻¹
|
|
14
|
+
tau(float): Non-radiative recombination lifetime in s
|
|
15
|
+
sigma(float): Spectral dispersion of the absorption coefficient spectrum, unitless
|
|
16
|
+
dos_mass(float): Density of States effective mass in m₀
|
|
17
|
+
dop_density(float): Doping density in cm⁻³
|
|
18
|
+
epsilon(float): Static dielectric constant, unitless
|
|
19
|
+
mu(float): Charge carrier mobility in cm²V⁻¹s⁻¹
|
|
20
|
+
Tcell(float): Operating temperature of the cell in K
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
efficiency(float): Percentage photovoltaic figure of merit efficiency.
|
|
24
|
+
'''
|
|
25
|
+
|
|
26
|
+
PV_FOM = Final_equation(E_gap, alpha, tau, sigma, dos_mass, dop_density, epsilon, mu)
|
|
27
|
+
|
|
28
|
+
k_1 = 3.3e-1
|
|
29
|
+
k_2 = 9.06e-2
|
|
30
|
+
k_3 = 2.48e-3
|
|
31
|
+
|
|
32
|
+
FOM_Pv_235 = PV_FOM ** (-0.235)
|
|
33
|
+
FOM_Pv_869 = PV_FOM ** 0.869
|
|
34
|
+
FOM_Pv_362 = PV_FOM ** (-0.362)
|
|
35
|
+
|
|
36
|
+
fraction = (k_1 * FOM_Pv_235) / (1 + k_2 * FOM_Pv_869)
|
|
37
|
+
denom_bracket = 1 + (k_3 * FOM_Pv_362)
|
|
38
|
+
|
|
39
|
+
SQ_eff = max_eff(E_gap, photon_spectrum, Tcell)
|
|
40
|
+
SQ = SQ_eff * 100
|
|
41
|
+
|
|
42
|
+
efficiency = SQ / ((1 + fraction) * denom_bracket)
|
|
43
|
+
|
|
44
|
+
return efficiency
|
|
45
|
+
|
|
46
|
+
# Crovetto efficiency realtive to SQ limit
|
|
47
|
+
|
|
48
|
+
def SQ_relative_Crovetto_efficiency(E_gap, photon_spectrum, alpha, tau, sigma, dos_mass, dop_density, epsilon, mu, Tcell):
|
|
49
|
+
|
|
50
|
+
''' Calculates the final value for the photovoltaic figure of merit relative to the SQ limit from Crovetto 2024
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
E_gap(float): Optical Band Gap in eV
|
|
54
|
+
photon_spectrum(numpy.ndarrray): Converted input spectrum from DB_FOM.convert_spectrum y: Number of photons (Np/m2/s/dE) x: Energy (eV)
|
|
55
|
+
alpha(float): Spectral average of incident light in cm⁻¹
|
|
56
|
+
tau(float): Non-radiative recombination lifetime in s
|
|
57
|
+
sigma(float): Spectral dispersion of the absorption coefficient spectrum, unitless
|
|
58
|
+
dos_mass(float): Density of States effective mass in m₀
|
|
59
|
+
dop_density(float): Doping density in cm⁻³
|
|
60
|
+
epsilon(float): Static dielectric constant, unitless
|
|
61
|
+
mu(float): Charge carrier mobility in cm²V⁻¹s⁻¹
|
|
62
|
+
Tcell(float): Operating temperature of the cell in K
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
efficiency(float): Percentage photovoltaic figure of merit efficiency relative to the SQ limit.
|
|
66
|
+
'''
|
|
67
|
+
|
|
68
|
+
Crovetto_eff = Crovetto_efficiency(E_gap, photon_spectrum, alpha, tau, sigma, dos_mass, dop_density, epsilon, mu, Tcell)
|
|
69
|
+
SQ = max_eff(E_gap, photon_spectrum, Tcell)
|
|
70
|
+
|
|
71
|
+
SQ_relative = Crovetto_eff / SQ
|
|
72
|
+
|
|
73
|
+
return SQ_relative
|
solphin/optics.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from pymatgen.io.vasp import Vasprun
|
|
2
|
+
import numpy as np
|
|
3
|
+
from os.path import join
|
|
4
|
+
import scipy.special as sc
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
q=1.60217662E-19
|
|
8
|
+
kT=0.0258519975 # eV for T=300K
|
|
9
|
+
k=0.000086173325 #eV/K
|
|
10
|
+
h=4.135667E-15 #eVs
|
|
11
|
+
c=2.9979E+8 #m/s
|
|
12
|
+
|
|
13
|
+
def calc_dielectric(filename):
|
|
14
|
+
|
|
15
|
+
'''Calculates the dielectric constants from a vasprun.xml
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
filename(string): filename/ path of the vasprun, typically vasprun.xml.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
eps_full(np.array): static dielectric constant (complex, contains both real and imaginary components)
|
|
22
|
+
energies(np.array): energy of the incident radiation eV
|
|
23
|
+
'''
|
|
24
|
+
|
|
25
|
+
load_vasprun = Vasprun(filename)
|
|
26
|
+
dielectric = load_vasprun.dielectric
|
|
27
|
+
|
|
28
|
+
energies = np.array(dielectric[0])
|
|
29
|
+
|
|
30
|
+
real_eps = np.array(dielectric[1])[:, [[0, 3, 5], [3, 1, 4], [5, 4, 2]]]
|
|
31
|
+
imag_eps = np.array(dielectric[2])[:, [[0, 3, 5], [3, 1, 4], [5, 4, 2]]]
|
|
32
|
+
eps_full = real_eps + 1j * imag_eps
|
|
33
|
+
|
|
34
|
+
return eps_full, energies
|
|
35
|
+
|
|
36
|
+
def calc_absorption(eps_full, energies):
|
|
37
|
+
|
|
38
|
+
'''Calculates the averages of the real and imaginary components of the refractive index, absorption, losses, real and imaginary components of the
|
|
39
|
+
static dielectric constant.
|
|
40
|
+
|
|
41
|
+
Parameters:
|
|
42
|
+
eps_full(np.array): static dielectric constant (complex, contains both real and imaginary components)
|
|
43
|
+
energies(np.array): energy of the incident radiation eV
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
data(dictionary):
|
|
47
|
+
'''
|
|
48
|
+
|
|
49
|
+
# take sqrt of eps matrix; if eps = V S V^-1; then eps^1/2 = V S^{1/2} V^-1;
|
|
50
|
+
eigvals, eigvecs = np.linalg.eig(eps_full)
|
|
51
|
+
|
|
52
|
+
# fancy einsum to calculate V S^{1/2} V^-1 at every energy
|
|
53
|
+
n = np.einsum("ijk,ik,ikl->ijl", eigvecs, np.sqrt(eigvals), np.linalg.inv(eigvecs))
|
|
54
|
+
|
|
55
|
+
# calculate optical absorption
|
|
56
|
+
alpha = n.imag * energies[:, None, None] * 4 * np.pi / 1.23984212e-4
|
|
57
|
+
|
|
58
|
+
# Invert epsilon to obtain energy-loss function
|
|
59
|
+
loss = -np.linalg.inv(eps_full).imag
|
|
60
|
+
|
|
61
|
+
eps = np.linalg.eigvals(eps_full).mean(axis=1)
|
|
62
|
+
n = np.linalg.eigvals(n).mean(axis=1)
|
|
63
|
+
loss = np.linalg.eigvalsh(loss).mean(axis=1)
|
|
64
|
+
alpha = np.linalg.eigvalsh(alpha).mean(axis=1)
|
|
65
|
+
|
|
66
|
+
data = {
|
|
67
|
+
"eps_real": eps.real,
|
|
68
|
+
"eps_imag": eps.imag,
|
|
69
|
+
"n_real": n.real,
|
|
70
|
+
"n_imag": n.imag,
|
|
71
|
+
"loss": loss,
|
|
72
|
+
"absorption": alpha,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return data
|
|
76
|
+
|
|
77
|
+
def print_n_real_file(data, energies):
|
|
78
|
+
|
|
79
|
+
filename = 'n_real.dat'
|
|
80
|
+
directory = 'dos'
|
|
81
|
+
|
|
82
|
+
if directory:
|
|
83
|
+
filename = join(directory, filename)
|
|
84
|
+
|
|
85
|
+
header = "energy(eV)"
|
|
86
|
+
|
|
87
|
+
header += " alpha"
|
|
88
|
+
data = np.stack((energies, data['n_real']), axis=1)
|
|
89
|
+
|
|
90
|
+
np.savetxt(filename, data, header=header)
|
|
91
|
+
|
|
92
|
+
def generate_n_real(filename):
|
|
93
|
+
|
|
94
|
+
eps_full, energies = calc_dielectric(filename)
|
|
95
|
+
|
|
96
|
+
data = calc_absorption(eps_full, energies)
|
|
97
|
+
|
|
98
|
+
print_n_real_file(data, energies)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def blank_flat(alpha, n, length):
|
|
102
|
+
|
|
103
|
+
#Absorptance for flat scatterer, from Matlab script
|
|
104
|
+
the_c = np.arcsin(1/n[0])
|
|
105
|
+
theta = np.linspace(0.0, the_c, num=200)
|
|
106
|
+
a_len = len(alpha)
|
|
107
|
+
t_len = len(theta)
|
|
108
|
+
toft = np.zeros(a_len*t_len)
|
|
109
|
+
toft = toft.reshape(a_len, t_len)
|
|
110
|
+
absorb_tr = np.zeros(a_len)
|
|
111
|
+
|
|
112
|
+
for i, a_pt in enumerate(alpha):
|
|
113
|
+
for j, t_pt in enumerate(theta):
|
|
114
|
+
toft_pt = np.exp(((2*np.multiply((-a_pt), length))/np.cos(t_pt)))
|
|
115
|
+
toft[i, j] = toft_pt
|
|
116
|
+
a_t_1 = np.trapz((toft[i,:]*np.cos(theta)*np.sin(theta)), theta)
|
|
117
|
+
a_t_2 = np.trapz((np.cos(theta)*np.sin(theta)), theta)
|
|
118
|
+
absorb_tr[i] = 1 - (a_t_1/a_t_2)
|
|
119
|
+
|
|
120
|
+
absorb = absorb_tr.conjugate()
|
|
121
|
+
return(absorb)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def blank_lambert(alpha, n, length):
|
|
125
|
+
|
|
126
|
+
#Absorptance for Lambertian scatterer coating, from Matlab script
|
|
127
|
+
x = np.multiply(2,np.multiply(alpha,length))
|
|
128
|
+
T = np.exp((-x)) - np.multiply(x, np.exp((-x))) + (x**2*sc.exp1(x))
|
|
129
|
+
R = 1.0/(np.multiply(n,n))
|
|
130
|
+
Abs = (1-T)/(1-T+(R*T))
|
|
131
|
+
Abs = np.nan_to_num(Abs)
|
|
132
|
+
Emi = (R*T)/(1-T+(R*T))
|
|
133
|
+
|
|
134
|
+
return(Abs)
|
|
135
|
+
|
|
136
|
+
def blank_eta(spectrum, E, alpha, n, length, Qi, trap):
|
|
137
|
+
#For given scatterer, calculates Blank et al. eta
|
|
138
|
+
dE = E[1]-E[0]
|
|
139
|
+
if trap == 1:
|
|
140
|
+
Abs = blank_flat(alpha, n, length)
|
|
141
|
+
elif trap == 2:
|
|
142
|
+
Abs = blank_lambert(alpha, n, length)
|
|
143
|
+
|
|
144
|
+
#np divide necessary to have array divide here?
|
|
145
|
+
phibb = 2*np.divide(np.divide(np.multiply(E,E),((h**3)*(c**2))),(np.exp(E/kT)-1))
|
|
146
|
+
phibb = np.nan_to_num(phibb) # NaNs to 0, as in Matlab
|
|
147
|
+
|
|
148
|
+
ps_E = spectrum[:, 0]
|
|
149
|
+
phi_sun = spectrum[:, 1]
|
|
150
|
+
|
|
151
|
+
# works for all E values above 0.03? won't extrapolate for 0
|
|
152
|
+
# values < gap shouldn't be relevant?
|
|
153
|
+
phisun = 10000*np.interp(E, ps_E, phi_sun)
|
|
154
|
+
|
|
155
|
+
Jsc = q*np.sum(Abs*phisun)*dE
|
|
156
|
+
J0rad = q*np.sum(Abs*phibb)*dE
|
|
157
|
+
|
|
158
|
+
Rrad = 4*np.pi*np.sum(alpha*(n**2)*phibb)*dE
|
|
159
|
+
Rnrad = (Rrad-Qi*Rrad)/Qi
|
|
160
|
+
|
|
161
|
+
pe = J0rad/(q*Rrad*length)
|
|
162
|
+
J0 = q*length*(Rnrad + pe*Rrad)
|
|
163
|
+
#Looks like this scans over only voltages between 0 and 2 V
|
|
164
|
+
#need testing for band gaps>2 eV?
|
|
165
|
+
V = np.linspace(0, 2, 1001)
|
|
166
|
+
Pmax = np.max(V*(Jsc - J0*(np.exp(V/kT)-1)))
|
|
167
|
+
eta = Pmax/1000
|
|
168
|
+
|
|
169
|
+
return(eta)
|
|
170
|
+
|
|
171
|
+
def blank_parse(folder):
|
|
172
|
+
|
|
173
|
+
# Parses outputs from current directory
|
|
174
|
+
|
|
175
|
+
abs_data = pd.read_table(f'{folder}/absorption.dat', delim_whitespace=True,
|
|
176
|
+
skiprows=1, header=None)
|
|
177
|
+
n_data = pd.read_table(f'{folder}/n_real.dat', delim_whitespace=True,
|
|
178
|
+
skiprows=1, header=None)
|
|
179
|
+
|
|
180
|
+
E_p = list(abs_data[0])
|
|
181
|
+
alpha_p = list(abs_data[1])
|
|
182
|
+
n_p = list(n_data[1])
|
|
183
|
+
|
|
184
|
+
return{"E": E_p, "alpha": alpha_p, "n": n_p}
|
|
185
|
+
|
|
186
|
+
def blank_calculate(spectrum, folder):
|
|
187
|
+
|
|
188
|
+
data = blank_parse(folder)
|
|
189
|
+
|
|
190
|
+
E = np.asarray(data["E"])
|
|
191
|
+
alpha = np.asarray(data["alpha"])
|
|
192
|
+
alpha = np.multiply(alpha, 100)
|
|
193
|
+
n = np.asarray(data["n"])
|
|
194
|
+
|
|
195
|
+
#Remove data for E>5eV, necessary for speed! Also done in Matlab
|
|
196
|
+
|
|
197
|
+
E = np.asarray([o for o in E if o <= 5])
|
|
198
|
+
alpha = alpha[0:(len(E))]
|
|
199
|
+
n = n[0:(len(E))]
|
|
200
|
+
|
|
201
|
+
#main, looping over lengths and Qi, outputs eta table
|
|
202
|
+
length_arr = np.logspace(-8.0, -3.0, num=36)
|
|
203
|
+
Qi_arr = np.logspace(0, -6, num=4)
|
|
204
|
+
trap = [1, 2]
|
|
205
|
+
|
|
206
|
+
for tr_pt in trap:
|
|
207
|
+
eta_arr = np.zeros(len(length_arr)*(len(Qi_arr)+1))
|
|
208
|
+
eta_arr = eta_arr.reshape(len(length_arr), (len(Qi_arr)+1))
|
|
209
|
+
for k, l_pt in enumerate(length_arr):
|
|
210
|
+
for l, q_pt in enumerate(Qi_arr):
|
|
211
|
+
eta_max = blank_eta(spectrum, E, alpha, n, l_pt, q_pt, tr_pt)
|
|
212
|
+
eta_arr[k, 0] = l_pt
|
|
213
|
+
eta_arr[k, l+1] = eta_max
|
|
214
|
+
if tr_pt == 1:
|
|
215
|
+
head="Thickness[m] \t Eta as fraction for Flat scatterer with Qi = 1.0, 0.01, 1E-4, 1E-6"
|
|
216
|
+
np.savetxt('flat_eta_out', eta_arr, header=head)
|
|
217
|
+
elif tr_pt == 2:
|
|
218
|
+
head="Thickness[m] \t Eta as fraction for Lambertian scatterer with Qi = 1.0, 0.01, 1E-4, 1E-6"
|
|
219
|
+
np.savetxt('lamb_eta_out', eta_arr, header=head)
|
|
220
|
+
|