pysolarcell 1.0.0__tar.gz
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.
- pysolarcell-1.0.0/PKG-INFO +18 -0
- pysolarcell-1.0.0/README.md +0 -0
- pysolarcell-1.0.0/pysolarcell/__init__.py +2 -0
- pysolarcell-1.0.0/pysolarcell/materials.py +69 -0
- pysolarcell-1.0.0/pysolarcell/solarcell.py +608 -0
- pysolarcell-1.0.0/pysolarcell.egg-info/PKG-INFO +18 -0
- pysolarcell-1.0.0/pysolarcell.egg-info/SOURCES.txt +10 -0
- pysolarcell-1.0.0/pysolarcell.egg-info/dependency_links.txt +1 -0
- pysolarcell-1.0.0/pysolarcell.egg-info/requires.txt +5 -0
- pysolarcell-1.0.0/pysolarcell.egg-info/top_level.txt +1 -0
- pysolarcell-1.0.0/setup.cfg +4 -0
- pysolarcell-1.0.0/setup.py +24 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pysolarcell
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Solar cell simulation software
|
|
5
|
+
Home-page: https://github.com/AustL/PySolarCell
|
|
6
|
+
Author: AustL
|
|
7
|
+
Author-email: 21chydra@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: sympy
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: pandas
|
|
17
|
+
Requires-Dist: matplotlib
|
|
18
|
+
Requires-Dist: scipy
|
|
File without changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import scipy.interpolate
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
class Material:
|
|
7
|
+
def __init__(self, name, bandgap):
|
|
8
|
+
self.name = name
|
|
9
|
+
self.bandgap = bandgap # eV
|
|
10
|
+
self.n = lambda x: 1
|
|
11
|
+
self.k = lambda x: 0
|
|
12
|
+
|
|
13
|
+
def setN(self, wavelengths, n):
|
|
14
|
+
"""Set the real refractive index
|
|
15
|
+
|
|
16
|
+
:param wavelengths: List of wavelengths (nm)
|
|
17
|
+
:param n: Refractive index at each wavelength
|
|
18
|
+
:return: Creates function n(lambda)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
self.n = scipy.interpolate.interp1d(wavelengths, n, fill_value=1, bounds_error=False)
|
|
22
|
+
|
|
23
|
+
def setK(self, wavelengths, k):
|
|
24
|
+
"""Set the extinction coefficient
|
|
25
|
+
|
|
26
|
+
:param wavelengths: List of wavelengths (nm)
|
|
27
|
+
:param k: Extinction coefficient at each wavelength
|
|
28
|
+
:return: Creates function k(lambda)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
self.k = scipy.interpolate.interp1d(wavelengths, k, fill_value=0, bounds_error=False)
|
|
32
|
+
|
|
33
|
+
def plot(self):
|
|
34
|
+
wavelengths = np.linspace(300, 1200, 500)
|
|
35
|
+
plt.plot(wavelengths, self.n(wavelengths), label='n')
|
|
36
|
+
plt.plot(wavelengths, self.k(wavelengths), label='k')
|
|
37
|
+
plt.xlabel('Wavelength (nm)')
|
|
38
|
+
plt.ylabel('n, k')
|
|
39
|
+
plt.legend()
|
|
40
|
+
plt.title(f'Optical Properties of {self.name}')
|
|
41
|
+
plt.show()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def PEROVSKITE():
|
|
45
|
+
n_data = pd.read_csv('materials/CsPbI3_PSK_n.txt', sep='\t', names=['Wavelength', 'n'])
|
|
46
|
+
k_data = pd.read_csv('materials/CsPbI3_PSK_k.txt', sep='\t', names=['Wavelength', 'k'])
|
|
47
|
+
|
|
48
|
+
material = Material('Perovskite', 1.65)
|
|
49
|
+
material.setN(n_data['Wavelength'] * 1e3, n_data['n'])
|
|
50
|
+
material.setK(k_data['Wavelength'] * 1e3, k_data['k'])
|
|
51
|
+
|
|
52
|
+
return material
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def SILICON():
|
|
56
|
+
n_data = pd.read_csv('materials/Si_n.txt', sep='\t', names=['Wavelength', 'n'])
|
|
57
|
+
k_data = pd.read_csv('materials/Si_k.txt', sep='\t', names=['Wavelength', 'k'])
|
|
58
|
+
|
|
59
|
+
material = Material('Silicon', 1.12)
|
|
60
|
+
material.setN(n_data['Wavelength'] * 1e3, n_data['n'])
|
|
61
|
+
material.setK(k_data['Wavelength'] * 1e3, k_data['k'])
|
|
62
|
+
|
|
63
|
+
return material
|
|
64
|
+
|
|
65
|
+
if __name__ == '__main__':
|
|
66
|
+
perovskite = PEROVSKITE()
|
|
67
|
+
perovskite.plot()
|
|
68
|
+
silicon = SILICON()
|
|
69
|
+
silicon.plot()
|
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import sympy as sp
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import scipy.integrate
|
|
6
|
+
import scipy.interpolate
|
|
7
|
+
|
|
8
|
+
from materials import Material, PEROVSKITE, SILICON
|
|
9
|
+
|
|
10
|
+
# Constants
|
|
11
|
+
h = 6.62607015e-34 # Js
|
|
12
|
+
c = 299792458 # m/s
|
|
13
|
+
k = 1.380649e-23 # J/K
|
|
14
|
+
q = 1.60217e-19 # C
|
|
15
|
+
sigma = 5.670374419e-8 # W/m^2/K^4
|
|
16
|
+
pi = np.pi
|
|
17
|
+
n_points = 100 # Change to 1000 for straight lines
|
|
18
|
+
|
|
19
|
+
np.seterr(all='raise')
|
|
20
|
+
|
|
21
|
+
AM15G_CONST = pd.read_csv('spectra/AM1.5G.txt', skiprows=1, sep='\t',
|
|
22
|
+
names=['Wavelength', 'AM0', 'Spectral Irradiance', 'AM1.5D'])
|
|
23
|
+
AM15D_CONST = pd.read_csv('spectra/AM1.5G.txt', skiprows=1, sep='\t',
|
|
24
|
+
names=['Wavelength', 'AM0', 'AM1.5G', 'Spectral Irradiance'])
|
|
25
|
+
AM0_CONST = pd.read_csv('spectra/AM1.5G.txt', skiprows=1, sep='\t',
|
|
26
|
+
names=['Wavelength','Spectral Irradiance','AM1.5G', 'AM1.5D'])
|
|
27
|
+
LED1_CONST = pd.read_csv('spectra/LED1.txt', skiprows=2, sep='\t',
|
|
28
|
+
names=['Wavelength','Spectral Irradiance'])
|
|
29
|
+
LED2_CONST = pd.read_csv('spectra/LED2.txt', skiprows=3, sep='\t',
|
|
30
|
+
names=['Wavelength','Spectral Irradiance'])
|
|
31
|
+
print(LED2_CONST.head())
|
|
32
|
+
|
|
33
|
+
def AM15G():
|
|
34
|
+
return AM15G_CONST.copy()
|
|
35
|
+
|
|
36
|
+
def AM15D():
|
|
37
|
+
return AM15D_CONST.copy()
|
|
38
|
+
|
|
39
|
+
def AM0():
|
|
40
|
+
return AM0_CONST.copy()
|
|
41
|
+
|
|
42
|
+
def LED1():
|
|
43
|
+
return LED1_CONST.copy()
|
|
44
|
+
|
|
45
|
+
def LED2():
|
|
46
|
+
return LED2_CONST.copy()
|
|
47
|
+
|
|
48
|
+
def PARALLEL(*cells):
|
|
49
|
+
return [*cells, True]
|
|
50
|
+
|
|
51
|
+
def SERIES(*cells):
|
|
52
|
+
return [*cells, False]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Layer:
|
|
56
|
+
def __init__(self, name, bandgap, iqe=1, thickness: float=0, k=None, area=100, Rs=0, Rsh=np.inf, T=298, n=1):
|
|
57
|
+
"""Creates a new layer of a solar cell stack. This can be created from a thickness and absorption data or a bandgap.
|
|
58
|
+
|
|
59
|
+
:param name: Name of the layer (str)
|
|
60
|
+
:param bandgap: Bandgap of the material (float)
|
|
61
|
+
:param iqe: Internal quantum efficiency (float)
|
|
62
|
+
:param thickness: Thickness of the layer (nm)
|
|
63
|
+
:param k: Extinction coefficient (unitless) as a function of wavelength (nm)
|
|
64
|
+
:param bandgap: Bandgap energy (eV)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
self.name = name
|
|
68
|
+
self.bandgap = bandgap # Note: Bandgap must be greater than 1.05 eV for 1200 nm
|
|
69
|
+
self.iqe = iqe
|
|
70
|
+
self.thickness = thickness
|
|
71
|
+
self.k = k
|
|
72
|
+
self.area = area
|
|
73
|
+
self.Rs = Rs
|
|
74
|
+
self.Rsh = Rsh
|
|
75
|
+
self.T = T
|
|
76
|
+
self.n = n
|
|
77
|
+
|
|
78
|
+
self.properties = {}
|
|
79
|
+
|
|
80
|
+
self.type = None
|
|
81
|
+
|
|
82
|
+
if self.thickness != 0 and self.k is not None:
|
|
83
|
+
self.type = 'absorption'
|
|
84
|
+
else:
|
|
85
|
+
self.type = 'bandgap'
|
|
86
|
+
|
|
87
|
+
def __repr__(self):
|
|
88
|
+
return f'Layer({self.name})'
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def fromMaterial(name, material: Material, thickness: float, *args):
|
|
92
|
+
"""Create a layer from a given material of some thickness
|
|
93
|
+
|
|
94
|
+
:param name: Name of the layer
|
|
95
|
+
:param material: Material of the layer
|
|
96
|
+
:param thickness: Thickness of the layer (nm)
|
|
97
|
+
:param args: Other parameters
|
|
98
|
+
:return: Layer of the given material
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
return Layer(name, material.bandgap, thickness, material.k, *args)
|
|
102
|
+
|
|
103
|
+
def toSolarCell(self, light_spectrum):
|
|
104
|
+
"""Converts the layer to a solar cell using a light spectrum
|
|
105
|
+
|
|
106
|
+
:param light_spectrum: The incident spectrum
|
|
107
|
+
:return: The solar cell from the layer AND changes the spectrum in-place
|
|
108
|
+
"""
|
|
109
|
+
Eg = self.bandgap * q # Energy in J
|
|
110
|
+
u = Eg / (k * self.T)
|
|
111
|
+
|
|
112
|
+
# Diode saturation current (mA/cm^2)
|
|
113
|
+
J0 = ((15 * q * sigma * self.T ** 3) / (k * pi ** 4)
|
|
114
|
+
* scipy.integrate.quad(lambda x: x ** 2 / (np.exp(x) - 1), u, 500)[0] / 10)
|
|
115
|
+
|
|
116
|
+
if self.type == 'bandgap':
|
|
117
|
+
lambda_g = h * c / Eg * 1e9 # Bandgap wavelength in nm
|
|
118
|
+
EQE = np.zeros_like(light_spectrum['Wavelength'], dtype=np.float64)
|
|
119
|
+
EQE[light_spectrum['Wavelength'] < lambda_g] = self.iqe
|
|
120
|
+
|
|
121
|
+
if self.type == 'absorption':
|
|
122
|
+
# I/I0 = exp(-4 pi k x / lambda) with x in nm and lambda in nm
|
|
123
|
+
EQE = (1 - np.exp(-4 * pi * self.k(light_spectrum['Wavelength']) * self.thickness / light_spectrum['Wavelength'])) * self.iqe
|
|
124
|
+
|
|
125
|
+
spectral_response = q * light_spectrum['Wavelength'] / (h * c) * EQE * 1e-9
|
|
126
|
+
|
|
127
|
+
Jsc = scipy.integrate.trapezoid(light_spectrum['Spectral Irradiance'] * spectral_response,
|
|
128
|
+
light_spectrum['Wavelength']) / 10 # mA/cm^2
|
|
129
|
+
|
|
130
|
+
Voc = self.n * k * self.T / q * np.log(Jsc / J0 + 1)
|
|
131
|
+
|
|
132
|
+
solar_cell = SolarCell(Jsc, Voc, area=self.area, Rs=self.Rs, Rsh=self.Rsh, T=self.T, n=self.n)
|
|
133
|
+
|
|
134
|
+
self.properties['Jsc'] = Jsc
|
|
135
|
+
self.properties['Voc'] = Voc
|
|
136
|
+
self.properties['JV'] = solar_cell.jv()
|
|
137
|
+
self.properties['FF'] = solar_cell.ff()
|
|
138
|
+
self.properties['MPP'] = solar_cell.mpp()
|
|
139
|
+
self.properties['EQE'] = (light_spectrum['Wavelength'], light_spectrum['Spectral Irradiance'] * EQE)
|
|
140
|
+
self.properties['Incident Power'] = scipy.integrate.trapezoid(light_spectrum['Spectral Irradiance'], light_spectrum['Wavelength']) / 10
|
|
141
|
+
|
|
142
|
+
# Calculate light after the cell
|
|
143
|
+
light_spectrum['Spectral Irradiance'] = light_spectrum['Spectral Irradiance'] * (1 - EQE)
|
|
144
|
+
|
|
145
|
+
return solar_cell
|
|
146
|
+
|
|
147
|
+
def eqe(self):
|
|
148
|
+
"""Returns the external quantum efficiency of the cell as a function of wavelength
|
|
149
|
+
|
|
150
|
+
:return: Quantum efficiency (%) as a function of wavelength
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
if 'EQE' not in self.properties.keys():
|
|
154
|
+
print('Warning: You must convert this layer to a solar cell first.')
|
|
155
|
+
|
|
156
|
+
EQE = self.properties['EQE']
|
|
157
|
+
return EQE[0], EQE[1] / AM15G()['Spectral Irradiance'] * 100
|
|
158
|
+
|
|
159
|
+
def jv(self):
|
|
160
|
+
"""Returns the JV curve of the cell as a tuple (voltages, currents)
|
|
161
|
+
|
|
162
|
+
:return: JV curve as a tuple (voltages, currents)
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
if 'JV' not in self.properties.keys():
|
|
166
|
+
print('Warning: You must convert this layer to a solar cell first.')
|
|
167
|
+
|
|
168
|
+
return self.properties['JV']
|
|
169
|
+
|
|
170
|
+
def ff(self):
|
|
171
|
+
if 'FF' not in self.properties.keys():
|
|
172
|
+
print('Warning: You must convert this layer to a solar cell first.')
|
|
173
|
+
|
|
174
|
+
return self.properties['FF']
|
|
175
|
+
|
|
176
|
+
def mpp(self):
|
|
177
|
+
if 'MPP' not in self.properties.keys():
|
|
178
|
+
print('Warning: You must convert this layer to a solar cell first.')
|
|
179
|
+
|
|
180
|
+
return self.properties['MPP']
|
|
181
|
+
|
|
182
|
+
def efficiency(self):
|
|
183
|
+
"""Returns the efficiency of the cell assuming AM1.5G spectrum
|
|
184
|
+
|
|
185
|
+
:return: Efficiency (%)
|
|
186
|
+
"""
|
|
187
|
+
if 'MPP' not in self.properties.keys():
|
|
188
|
+
print('Warning: You must convert this layer to a solar cell first.')
|
|
189
|
+
|
|
190
|
+
return self.properties['MPP'][1] / self.properties['Incident Power'] * 100
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class Stack:
|
|
194
|
+
def __init__(self, layers, lamp=None, name=''):
|
|
195
|
+
"""Creates a stack of cells. Usage SERIES(layer1, PARALLEL(layer2, layer3))
|
|
196
|
+
|
|
197
|
+
:param layers: List of layers
|
|
198
|
+
:param lamp: The initial light source
|
|
199
|
+
:param name: The name of the stack
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
self.name = name
|
|
203
|
+
self.layers = layers
|
|
204
|
+
self.lamp = lamp
|
|
205
|
+
self.flattened_layers = []
|
|
206
|
+
|
|
207
|
+
if self.lamp is None:
|
|
208
|
+
self.lamp = AM15G()
|
|
209
|
+
|
|
210
|
+
self.incident_power = scipy.integrate.trapezoid(self.lamp['Spectral Irradiance'], self.lamp['Wavelength']) / 10
|
|
211
|
+
|
|
212
|
+
self.jv_curve = None
|
|
213
|
+
self.properties = {}
|
|
214
|
+
|
|
215
|
+
def flatten(self, layers, parallel=False):
|
|
216
|
+
if isinstance(layers, Layer):
|
|
217
|
+
self.flattened_layers.append(layers)
|
|
218
|
+
|
|
219
|
+
# Convert layer to IV curve
|
|
220
|
+
cell = layers.toSolarCell(self.lamp)
|
|
221
|
+
|
|
222
|
+
# if parallel:
|
|
223
|
+
# return cell.jv()
|
|
224
|
+
|
|
225
|
+
currents = cell.currents
|
|
226
|
+
voltages = cell.V(currents)
|
|
227
|
+
return voltages, cell.ItoJ(currents)
|
|
228
|
+
|
|
229
|
+
if isinstance(layers, list):
|
|
230
|
+
assert len(layers) == 3, 'Stack formatted incorrectly'
|
|
231
|
+
|
|
232
|
+
jv1 = self.flatten(layers[0], parallel=layers[2])
|
|
233
|
+
jv2 = self.flatten(layers[1], parallel=layers[2])
|
|
234
|
+
if layers[2]:
|
|
235
|
+
return Stack.add_jv_parallel(jv1, jv2)
|
|
236
|
+
|
|
237
|
+
return Stack.add_jv_series(jv1, jv2)
|
|
238
|
+
|
|
239
|
+
raise ValueError('Stack formatted incorrectly')
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def jv(self):
|
|
243
|
+
# Use precomputed JV curve
|
|
244
|
+
if 'JV' not in self.properties.keys():
|
|
245
|
+
self.properties['JV'] = self.flatten(self.layers)
|
|
246
|
+
|
|
247
|
+
return self.properties['JV']
|
|
248
|
+
|
|
249
|
+
def eqe(self):
|
|
250
|
+
if 'EQE' not in self.properties.keys():
|
|
251
|
+
wavelengths = None
|
|
252
|
+
EQE = None
|
|
253
|
+
for layer in self.flattened_layers:
|
|
254
|
+
if EQE is None:
|
|
255
|
+
wavelengths, EQE = layer.eqe()
|
|
256
|
+
else:
|
|
257
|
+
EQE += layer.eqe()[1]
|
|
258
|
+
|
|
259
|
+
self.properties['EQE'] = (wavelengths, EQE)
|
|
260
|
+
|
|
261
|
+
return self.properties['EQE']
|
|
262
|
+
|
|
263
|
+
def mpp(self):
|
|
264
|
+
"""Finds the maximum power point of the solar cell stack
|
|
265
|
+
|
|
266
|
+
:return: Tuple of ((voltage, current), maximum power)
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
if 'MPP' not in self.properties.keys():
|
|
270
|
+
|
|
271
|
+
voltages, currents = self.jv()
|
|
272
|
+
|
|
273
|
+
max_index = np.argmax(voltages * currents)
|
|
274
|
+
|
|
275
|
+
self.properties['MPP'] = ((voltages[max_index], currents[max_index]),
|
|
276
|
+
voltages[max_index] * currents[max_index])
|
|
277
|
+
|
|
278
|
+
return self.properties['MPP']
|
|
279
|
+
|
|
280
|
+
def ff(self):
|
|
281
|
+
"""Finds the fill factor of the solar cell stack
|
|
282
|
+
|
|
283
|
+
:return: Fill factor of the solar cell stack (%)
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
if 'FF' not in self.properties.keys():
|
|
287
|
+
voltages, currents = self.jv()
|
|
288
|
+
jv_func = scipy.interpolate.interp1d(voltages, currents, fill_value='extrapolate')
|
|
289
|
+
jsc = jv_func(0)
|
|
290
|
+
voc = voltages[np.argmin(np.abs(currents))]
|
|
291
|
+
|
|
292
|
+
self.properties['Jsc'] = jsc
|
|
293
|
+
self.properties['Voc'] = voc
|
|
294
|
+
|
|
295
|
+
self.properties['FF'] = self.mpp()[1] / (jsc * voc) * 100
|
|
296
|
+
|
|
297
|
+
return self.properties['FF']
|
|
298
|
+
|
|
299
|
+
def efficiency(self):
|
|
300
|
+
return self.mpp()[1] / self.incident_power * 100
|
|
301
|
+
|
|
302
|
+
def solve(self):
|
|
303
|
+
self.jv()
|
|
304
|
+
self.eqe()
|
|
305
|
+
self.mpp()
|
|
306
|
+
self.ff()
|
|
307
|
+
|
|
308
|
+
print('-' * 6 + self.name + '-' * 6)
|
|
309
|
+
print(f'Voc = {self.properties['Voc']:.2f} V')
|
|
310
|
+
print(f'Jsc = {self.properties['Jsc']:.2f} mA/cm^2')
|
|
311
|
+
print(f'Efficiency = {self.efficiency():.2f}%')
|
|
312
|
+
print(f'FF = {self.properties['FF']:.2f}%')
|
|
313
|
+
print('-' * (len(self.name) + 12))
|
|
314
|
+
|
|
315
|
+
return self.properties
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def add_jv_parallel(jv1, jv2):
|
|
320
|
+
"""Adds IV curves (tuples of voltage, current) in parallel
|
|
321
|
+
|
|
322
|
+
:param jv1: First IV curve
|
|
323
|
+
:param jv2: Second IV curve
|
|
324
|
+
:return: New IV curve (tuple of voltage, current)
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
j1 = scipy.interpolate.interp1d(*jv1, fill_value='extrapolate')
|
|
328
|
+
j2 = scipy.interpolate.interp1d(*jv2, fill_value='extrapolate')
|
|
329
|
+
|
|
330
|
+
voltages = np.linspace(0, min(max(jv1[0]), max(jv2[0])) + 0.05, n_points)
|
|
331
|
+
j = j1(voltages) + j2(voltages)
|
|
332
|
+
|
|
333
|
+
return voltages, j
|
|
334
|
+
|
|
335
|
+
@staticmethod
|
|
336
|
+
def add_jv_series(jv1, jv2):
|
|
337
|
+
"""Adds IV curves (tuples of voltage, current) in series
|
|
338
|
+
|
|
339
|
+
:param jv1: First IV curve
|
|
340
|
+
:param jv2: Second IV curve
|
|
341
|
+
:return: New IV curve (tuple of voltage, current)
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
v1 = scipy.interpolate.interp1d(jv1[1], jv1[0], fill_value='extrapolate')
|
|
345
|
+
v2 = scipy.interpolate.interp1d(jv2[1], jv2[0], fill_value='extrapolate')
|
|
346
|
+
|
|
347
|
+
currents = np.linspace(0, min(max(jv1[1]), max(jv2[1])) + 0.05, n_points)
|
|
348
|
+
voltages = v1(currents) + v2(currents)
|
|
349
|
+
|
|
350
|
+
return voltages, currents
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class SolarCell:
|
|
354
|
+
def __init__(self, Jsc, Voc, area=100, Rs=0, Rsh=np.inf, T=298, n=1):
|
|
355
|
+
"""Class representing a solar cell
|
|
356
|
+
|
|
357
|
+
:param Jsc: Ideal short circuit current density (mA/cm^2)
|
|
358
|
+
:param Voc: Ideal open circuit voltage (V)
|
|
359
|
+
:param area: Area of cell (cm^2)
|
|
360
|
+
:param Rs: Series resistance (Ohm cm^2)
|
|
361
|
+
:param Rsh: Shunt resistance (Ohm cm^2)
|
|
362
|
+
:param T: Temperature
|
|
363
|
+
:param n: Ideality factor
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
self.Isc = Jsc * area / 1000 # Ideal short circuit current in mA/cm^2
|
|
367
|
+
self.Jsc = Jsc
|
|
368
|
+
self.Voc = Voc # Ideal open circuit voltage in V
|
|
369
|
+
self.area = area # Area in cm^2
|
|
370
|
+
self.Rs = Rs / area
|
|
371
|
+
self.Rsh = Rsh / area
|
|
372
|
+
self.T = T
|
|
373
|
+
self.n = n
|
|
374
|
+
|
|
375
|
+
self.voltages = np.linspace(0, self.Voc, n_points)
|
|
376
|
+
self.currents = np.linspace(0, self.Isc, n_points)
|
|
377
|
+
self.I0 = self.Isc / (np.exp(q * self.Voc / (self.n * k * self.T)))
|
|
378
|
+
|
|
379
|
+
self.I = None
|
|
380
|
+
self.V = None
|
|
381
|
+
self.calculate_currents()
|
|
382
|
+
self.calculate_voltages()
|
|
383
|
+
|
|
384
|
+
def calculate_currents(self):
|
|
385
|
+
"""Returns an array of currents
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
if self.Rs == 0 and self.Rsh == np.inf:
|
|
389
|
+
self.I = lambda v: self.Isc - self.I0 * (np.exp(q * v / (self.n * k * self.T)) - 1)
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
I = sp.Symbol('I')
|
|
393
|
+
result = np.zeros_like(self.voltages, dtype=np.float64)
|
|
394
|
+
|
|
395
|
+
for index, v in enumerate(self.voltages):
|
|
396
|
+
guess = self.Isc - self.I0 * (sp.exp(q * v / (self.n * k * self.T)) - 1)
|
|
397
|
+
result[index] = sp.nsolve(self.Isc
|
|
398
|
+
- self.I0 * (sp.exp((v + I * self.Rs) / (self.n * k * self.T / q)) - 1)
|
|
399
|
+
- (v + I * self.Rs) / self.Rsh - I, guess)
|
|
400
|
+
|
|
401
|
+
self.I = scipy.interpolate.interp1d(self.voltages, result, fill_value='extrapolate')
|
|
402
|
+
|
|
403
|
+
def calculate_voltages(self):
|
|
404
|
+
"""Returns an array of voltages
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
if self.Rs == 0 and self.Rsh == np.inf:
|
|
408
|
+
self.V = lambda i: (self.n * k * self.T) / q * np.log((self.Isc - i) / self.I0 + 1)
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
V = sp.Symbol('V')
|
|
412
|
+
result = np.zeros_like(self.currents, dtype=np.float64)
|
|
413
|
+
|
|
414
|
+
for index, i in enumerate(self.currents):
|
|
415
|
+
guess = (self.n * k * self.T) / q * sp.log((self.Isc - i) / self.I0 + 1)
|
|
416
|
+
result[index] = sp.nsolve(self.Isc
|
|
417
|
+
- self.I0 * (sp.exp((V + i * self.Rs) / (self.n * k * self.T / q)) - 1)
|
|
418
|
+
- (V + i * self.Rs) / self.Rsh - i, guess)
|
|
419
|
+
|
|
420
|
+
self.V = scipy.interpolate.interp1d(self.currents, result, fill_value='extrapolate')
|
|
421
|
+
|
|
422
|
+
def jv(self):
|
|
423
|
+
I = self.I(self.voltages)
|
|
424
|
+
|
|
425
|
+
return self.voltages, self.ItoJ(I)
|
|
426
|
+
|
|
427
|
+
def mpp(self):
|
|
428
|
+
"""Finds the maximum power point of the solar cell
|
|
429
|
+
|
|
430
|
+
:return: Tuple of ((voltage, current), maximum power)
|
|
431
|
+
"""
|
|
432
|
+
currents = self.I(self.voltages)
|
|
433
|
+
max_index = np.argmax(self.voltages * currents)
|
|
434
|
+
|
|
435
|
+
return (self.voltages[max_index], self.ItoJ(currents[max_index])), self.voltages[max_index] * currents[
|
|
436
|
+
max_index]
|
|
437
|
+
|
|
438
|
+
def ff(self):
|
|
439
|
+
"""Finds the fill factor of the solar cell
|
|
440
|
+
|
|
441
|
+
:return: Fill factor of the solar cell (%)
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
voltages, currents = self.jv()
|
|
445
|
+
jv_func = scipy.interpolate.interp1d(voltages, currents, fill_value='extrapolate')
|
|
446
|
+
jsc = jv_func(0)
|
|
447
|
+
voc = voltages[np.argmin(np.abs(currents))]
|
|
448
|
+
|
|
449
|
+
return self.mpp()[1] / (jsc * voc) * 100
|
|
450
|
+
|
|
451
|
+
def ItoJ(self, current):
|
|
452
|
+
return current * 1000 / self.area
|
|
453
|
+
|
|
454
|
+
@staticmethod
|
|
455
|
+
def mpp_from_jv(voltages, currents):
|
|
456
|
+
"""Finds the maximum power point from a JV curve
|
|
457
|
+
|
|
458
|
+
:param voltages: Voltage points
|
|
459
|
+
:param currents: Current points (mA/cm^2)
|
|
460
|
+
:return: Tuple of ((voltage, current), maximum power)
|
|
461
|
+
"""
|
|
462
|
+
max_index = np.argmax(voltages * currents)
|
|
463
|
+
max_power = voltages[max_index] * currents[max_index]
|
|
464
|
+
# print(f'Maximum Power: {max_power} W')
|
|
465
|
+
|
|
466
|
+
return (voltages[max_index], currents[max_index]), max_power
|
|
467
|
+
|
|
468
|
+
@staticmethod
|
|
469
|
+
def add_parallel(cell1: 'SolarCell', cell2: 'SolarCell'):
|
|
470
|
+
"""Returns the JV curve of two solar cells in parallel
|
|
471
|
+
|
|
472
|
+
:param cell1: First solar cell
|
|
473
|
+
:param cell2: Second solar cell
|
|
474
|
+
:return: Tuple of (voltages, currents)
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
voltages = np.linspace(0, min(cell1.Voc, cell2.Voc) + 0.05, n_points)
|
|
478
|
+
currents = cell1.ItoJ(cell1.I(voltages)) + cell2.ItoJ(cell2.I(voltages))
|
|
479
|
+
|
|
480
|
+
return voltages, currents
|
|
481
|
+
|
|
482
|
+
@staticmethod
|
|
483
|
+
def add_series(cell1: 'SolarCell', cell2: 'SolarCell'):
|
|
484
|
+
"""Returns the JV curve of two solar cells in series
|
|
485
|
+
|
|
486
|
+
:param cell1: First solar cell
|
|
487
|
+
:param cell2: Second solar cell
|
|
488
|
+
:return: Tuple of (voltages, currents)
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
currents = np.linspace(0, min(cell1.Isc, cell2.Isc) + 0.05, n_points)
|
|
492
|
+
voltages = cell1.V(currents) + cell2.V(currents)
|
|
493
|
+
|
|
494
|
+
return voltages, cell1.ItoJ(currents)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def plot_iv(*layers):
|
|
498
|
+
"""Plots the JV curve for all layers in layers
|
|
499
|
+
|
|
500
|
+
:param layers: The list of layers or stacks to plot
|
|
501
|
+
:return: Figure and axes for further modification
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
fig = plt.figure()
|
|
505
|
+
ax = fig.add_subplot(111)
|
|
506
|
+
|
|
507
|
+
for layer in layers:
|
|
508
|
+
ax.plot(*layer.jv(), label=layer.name)
|
|
509
|
+
|
|
510
|
+
ax.set_xlim(left=0)
|
|
511
|
+
ax.set_ylim(bottom=0)
|
|
512
|
+
ax.legend(loc='best')
|
|
513
|
+
ax.set_xlabel('Voltage (V)')
|
|
514
|
+
ax.set_ylabel('Current Density ($mA/cm^2$)')
|
|
515
|
+
fig.tight_layout()
|
|
516
|
+
|
|
517
|
+
return fig, ax
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def plot_eqe(*layers):
|
|
521
|
+
"""Plots the external quantum efficiency for all layers in layers
|
|
522
|
+
|
|
523
|
+
:param layers: The list of layers or stacks to plot
|
|
524
|
+
:return: Figure and axes for further modification
|
|
525
|
+
"""
|
|
526
|
+
fig = plt.figure()
|
|
527
|
+
ax = fig.add_subplot(111)
|
|
528
|
+
|
|
529
|
+
for layer in layers:
|
|
530
|
+
ax.plot(*layer.eqe(), label=layer.name)
|
|
531
|
+
ax.fill_between(*layer.eqe(), 0, alpha=0.2)
|
|
532
|
+
|
|
533
|
+
ax.legend()
|
|
534
|
+
ax.set_xlabel('Wavelength (nm)')
|
|
535
|
+
ax.set_ylabel('EQE (%)')
|
|
536
|
+
fig.tight_layout()
|
|
537
|
+
|
|
538
|
+
return fig, ax
|
|
539
|
+
|
|
540
|
+
if __name__ == '__main__':
|
|
541
|
+
# layer1 = Layer('Cell 1 (3.00 eV)', bandgap=1.63, iqe=1, Rs=0, Rsh=np.inf)
|
|
542
|
+
# layer2 = Layer('Cell 2 (1.77 eV)', bandgap=0.97, iqe=1, Rs=0, Rsh=np.inf)
|
|
543
|
+
# # layer1 = Layer('Cell 1 (3.00 eV)', bandgap=1.63, iqe=1)
|
|
544
|
+
# # layer2 = Layer('Cell 2 (1.77 eV)', bandgap=0.97, iqe=1)
|
|
545
|
+
#
|
|
546
|
+
# parallel = Stack(PARALLEL(layer1, layer2), name='Parallel')
|
|
547
|
+
# series = Stack(SERIES(layer1, layer2), name='Series')
|
|
548
|
+
# # cell1 = Stack(layer1, name='Cell 1')
|
|
549
|
+
# # cell2 = Stack(layer2, name='Cell 2')
|
|
550
|
+
#
|
|
551
|
+
# # parallel.solve()
|
|
552
|
+
# series.solve()
|
|
553
|
+
# # cell1.solve()
|
|
554
|
+
# # cell2.solve()
|
|
555
|
+
#
|
|
556
|
+
# fig, ax = plot_iv(layer1, layer2, parallel, series)
|
|
557
|
+
# # ax.set_xlim([0, 2])
|
|
558
|
+
# # plot_iv(cell1)
|
|
559
|
+
# plt.show()
|
|
560
|
+
#
|
|
561
|
+
# plot_eqe(layer1, layer2, parallel)
|
|
562
|
+
# plt.show()
|
|
563
|
+
|
|
564
|
+
# bandgaps = np.linspace(0.77, 3, 40)
|
|
565
|
+
# powers = np.zeros_like(bandgaps)
|
|
566
|
+
# for i, bandgap in enumerate(bandgaps):
|
|
567
|
+
# layer = Layer('Cell', bandgap=bandgap, iqe=1, Rs=0, Rsh=np.inf)
|
|
568
|
+
# cell = Stack(layer, 'Stack')
|
|
569
|
+
#
|
|
570
|
+
# powers[i] = cell.efficiency()
|
|
571
|
+
|
|
572
|
+
# plt.plot(bandgaps, powers)
|
|
573
|
+
#
|
|
574
|
+
# cell1 = Layer('Cell 1', 1.4605263157894737, iqe=1)
|
|
575
|
+
# cell2 = Layer('Cell 2',1.3578947368421053, iqe=1)
|
|
576
|
+
# cell3 = Layer('Cell 3', 1.2552631578947369, iqe=1)
|
|
577
|
+
# parallel = Stack(PARALLEL(PARALLEL(cell1, cell2), cell3))
|
|
578
|
+
# series = Stack(SERIES(SERIES(cell1, cell2), cell3))
|
|
579
|
+
#
|
|
580
|
+
# mixed1 = Stack(PARALLEL(SERIES(cell1, cell2), cell3))
|
|
581
|
+
# mixed2 = Stack(SERIES(PARALLEL(cell1, cell2), cell3))
|
|
582
|
+
# mixed3 = Stack(PARALLEL(cell1, SERIES(cell2, cell3)))
|
|
583
|
+
# mixed4 = Stack(SERIES(cell1, PARALLEL(cell2, cell3)))
|
|
584
|
+
#
|
|
585
|
+
# mixed4.solve()
|
|
586
|
+
|
|
587
|
+
layer1 = Layer('Cell 1 (3.00 eV)', bandgap=1.63, iqe=1, Rs=0, Rsh=np.inf)
|
|
588
|
+
layer2 = Layer('Cell 2 (1.77 eV)', bandgap=0.96, iqe=1, Rs=0, Rsh=np.inf)
|
|
589
|
+
|
|
590
|
+
parallel = Stack(PARALLEL(layer1, layer2), name='Parallel')
|
|
591
|
+
series = Stack(SERIES(layer1, layer2), name='Series')
|
|
592
|
+
# cell1 = Stack(layer1, name='Cell 1')
|
|
593
|
+
# cell2 = Stack(layer2, name='Cell 2')
|
|
594
|
+
|
|
595
|
+
parallel.solve()
|
|
596
|
+
series.solve()
|
|
597
|
+
# cell1.solve()
|
|
598
|
+
# cell2.solve()
|
|
599
|
+
|
|
600
|
+
fig, ax = plot_iv(layer1, layer2, parallel, series)
|
|
601
|
+
# ax.set_xlim([0, 2])
|
|
602
|
+
# plot_iv(cell1)
|
|
603
|
+
plt.show()
|
|
604
|
+
|
|
605
|
+
plot_eqe(layer1, layer2, parallel)
|
|
606
|
+
plt.show()
|
|
607
|
+
|
|
608
|
+
# TODO: Reflectance, recombination, diffusion length
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pysolarcell
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Solar cell simulation software
|
|
5
|
+
Home-page: https://github.com/AustL/PySolarCell
|
|
6
|
+
Author: AustL
|
|
7
|
+
Author-email: 21chydra@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: sympy
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: pandas
|
|
17
|
+
Requires-Dist: matplotlib
|
|
18
|
+
Requires-Dist: scipy
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
pysolarcell/__init__.py
|
|
4
|
+
pysolarcell/materials.py
|
|
5
|
+
pysolarcell/solarcell.py
|
|
6
|
+
pysolarcell.egg-info/PKG-INFO
|
|
7
|
+
pysolarcell.egg-info/SOURCES.txt
|
|
8
|
+
pysolarcell.egg-info/dependency_links.txt
|
|
9
|
+
pysolarcell.egg-info/requires.txt
|
|
10
|
+
pysolarcell.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pysolarcell
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import setuptools
|
|
2
|
+
|
|
3
|
+
with open('README.md', 'r') as f:
|
|
4
|
+
longDescription = f.read()
|
|
5
|
+
|
|
6
|
+
setuptools.setup(
|
|
7
|
+
name='pysolarcell',
|
|
8
|
+
version='1.0.0',
|
|
9
|
+
author='AustL',
|
|
10
|
+
author_email='21chydra@gmail.com',
|
|
11
|
+
description='Solar cell simulation software',
|
|
12
|
+
long_description=longDescription,
|
|
13
|
+
long_description_content_type='text/markdown',
|
|
14
|
+
url='https://github.com/AustL/PySolarCell',
|
|
15
|
+
packages=setuptools.find_packages(),
|
|
16
|
+
classifiers=[
|
|
17
|
+
'Programming Language :: Python :: 3',
|
|
18
|
+
'License :: OSI Approved :: MIT License',
|
|
19
|
+
'Operating System :: OS Independent'
|
|
20
|
+
],
|
|
21
|
+
python_requires='>=3.12',
|
|
22
|
+
license='MIT',
|
|
23
|
+
install_requires=['sympy', 'numpy', 'pandas', 'matplotlib', 'scipy']
|
|
24
|
+
)
|