/
solar_cell_solver.py
executable file
·410 lines (331 loc) · 14.2 KB
/
solar_cell_solver.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
from logging import getLogger
from typing import Dict, Union
from warnings import warn
import numpy as np
from . import analytic_solar_cells as ASC
from .light_source import LightSource
from .optics import ( # noqa
rcwa_options,
solve_beer_lambert,
solve_external_optics,
solve_rcwa,
solve_tmm,
)
from .registries import (
ACTIONS_REGISTRY,
register_action,
OPTICS_METHOD_REGISTRY,
SHORT_CIRCUIT_SOLVER_REGISTRY,
EQUILIBRIUM_SOLVER_REGISTRY,
)
from .solar_cell import SolarCell
from .state import State
from .structure import Junction, Layer, TunnelJunction
try:
from . import poisson_drift_diffusion as PDD
a = PDD.pdd_options
except AttributeError:
PDD.pdd_options = {}
default_options = State()
pdd_options = PDD.pdd_options
asc_options = ASC.db_options
def merge_dicts(*dict_args):
"""
Given any number of dicts, shallow copy and merge into a new dict,
precedence goes to key value pairs in latter dicts.
"""
result = State()
for dictionary in dict_args:
result.update(dictionary)
return result
# General
default_options.T_ambient = 298
default_options.T = 298
# Illumination spectrum
default_options.wavelength = np.linspace(300, 1800, 251) * 1e-9
default_options.light_source = LightSource(
source_type="standard",
version="AM1.5g",
x=default_options.wavelength,
output_units="photon_flux_per_m",
)
# IV control
default_options.voltages = np.linspace(0, 1.2, 100)
default_options.mpp = False
default_options.light_iv = False
default_options.internal_voltages = np.linspace(-6, 4, 1000)
default_options.position = None
default_options.radiative_coupling = False
# Optics control
default_options.optics_method = "BL"
default_options.recalculate_absorption = False
default_options = merge_dicts(
default_options, ASC.db_options, ASC.da_options, PDD.pdd_options, rcwa_options
)
def solar_cell_solver(
solar_cell: SolarCell, task: str, user_options: Union[Dict, State, None] = None
):
"""Solves the properties of a solar cell object
This can be done either calculating its optical properties (R, A and T), its quantum
efficiency or its current voltage characteristics in the dark or under illumination.
The general options for the solvers are passed as dictionaries or state objects.
Args:
- solar_cell: A solar_cell object
- taks: Task to perform. Some of the existing tasks might not be available for
all types of junctions.
- user_options: A dictionary containing the options for the solver, which will
overwrite the default options.
Return:
None
"""
if type(user_options) in [State, dict]:
options = merge_dicts(default_options, user_options)
else:
options = merge_dicts(default_options)
prepare_solar_cell(solar_cell, options)
options.T = solar_cell.T
action = ACTIONS_REGISTRY.get(task, None)
if action is None:
raise ValueError(
"ERROR in 'solar_cell_solver' - Valid tasks are "
f"{list(ACTIONS_REGISTRY.keys())}."
)
action(solar_cell, options)
@register_action("optics")
def solve_optics(solar_cell: SolarCell, options: State):
"""Solves the optical properties of the structure.
It calls one of the registered optics methods, defined by the options.optics_method
to calculate the reflection, absorption and transmission of the cell as well as
ligth abosrbed per layer and junction. Note that not all optic methods are
compatible with all junctions. Check the information spefific for each of them.
Args:
solar_cell: A solar_cell object
options: Options for the optics solver
Return:
None
"""
getLogger().info("Solving optics of the solar cell...")
calculated = hasattr(solar_cell[0], "absorbed")
recalc = options.get("recalculate_absorption", False)
if not calculated or recalc:
method = OPTICS_METHOD_REGISTRY.get(options.optics_method, None)
if method is None:
raise ValueError(
"ERROR in 'solar_cell_solver' - Valid optics methods are "
f"{list(OPTICS_METHOD_REGISTRY.keys())}."
)
method(solar_cell, **options)
else:
getLogger().info(
"Already calculated reflection, transmission and absorption profile - "
"not recalculating. Set 'recalculate_absorption' to True in the options if "
"you want absorption to be calculated again."
)
@register_action("iv")
def solve_iv(solar_cell, options):
"""Calculates the IV at a given voltage range, providing the IVs of the individual junctions in addition to the total IV
:param solar_cell: A solar_cell object
:param options: Options for the solvers
:return: None
"""
solve_optics(solar_cell, options)
print("Solving IV of the junctions...")
for j in solar_cell.junction_indices:
if solar_cell[j].kind == "PDD":
PDD.iv_pdd(solar_cell[j], **options)
elif solar_cell[j].kind == "DA":
ASC.iv_depletion(solar_cell[j], options)
elif solar_cell[j].kind == "2D":
ASC.iv_2diode(solar_cell[j], options)
elif solar_cell[j].kind == "DB":
ASC.iv_detailed_balance(solar_cell[j], options)
else:
raise ValueError(
'ERROR in "solar_cell_solver":\n\tJunction {} has an invalid "type". It must be "PDD", "DA", "2D" or "DB".'.format(
j
)
)
print("Solving IV of the tunnel junctions...")
for j in solar_cell.tunnel_indices:
if solar_cell[j].kind == "resistive":
# The tunnel junction is modeled as a simple resistor
ASC.resistive_tunnel_junction(solar_cell[j], options)
elif solar_cell[j].kind == "parametric":
# The tunnel junction is modeled using a simple parametric model
ASC.parametric_tunnel_junction(solar_cell[j], options)
elif solar_cell[j].kind == "external":
# The tunnel junction is modeled using a simple parametric model
ASC.external_tunnel_junction(solar_cell[j], options)
elif solar_cell[j].kind == "analytic":
print(
"Sorry, the analytical tunnel junction model is not implemented, yet."
)
else:
raise ValueError(
'ERROR in "solar_cell_solver":\n\tTunnel junction {} has an invalid "type". It must be "parametric", "analytic", "external" or "resistive".'.format(
j
)
)
print("Solving IV of the total solar cell...")
ASC.iv_multijunction(solar_cell, options)
@register_action("qe")
def solve_qe(solar_cell, options):
"""Calculates the QE of all the junctions
:param solar_cell: A solar_cell object
:param options: Options for the solvers
:return: None
"""
solve_optics(solar_cell, options)
print("Solving QE of the solar cell...")
for j in solar_cell.junction_indices:
if solar_cell[j].kind == "PDD":
PDD.qe_pdd(solar_cell[j], options)
elif solar_cell[j].kind == "DA":
ASC.qe_depletion(solar_cell[j], options)
elif solar_cell[j].kind == "2D":
# We solve this case as if it were DB. Therefore, to work it needs the same inputs in the Junction object
wl = options.wavelength
ASC.qe_detailed_balance(solar_cell[j], wl)
elif solar_cell[j].kind == "DB":
wl = options.wavelength
ASC.qe_detailed_balance(solar_cell[j], wl)
else:
raise ValueError(
'ERROR in "solar_cell_solver":\n\tJunction {} has an invalid "type". It must be "PDD", "DA", "2D" or "DB".'.format(
j
)
)
@register_action("equilibrium")
def solve_equilibrium(solar_cell: SolarCell, options: State):
"""Solves the electronic properfies of the cell at equilibrium conditons.
The junction objects are updated with the bandstructure and recombination profiles.
Args:
solar_cell: The solar cell to solve.
options: Options required by the solver.
"""
for j in solar_cell.junction_indices:
solver = EQUILIBRIUM_SOLVER_REGISTRY.get(solar_cell[j].kind, None)
if solver is None:
warn(
"ERROR in 'solve_equilibrium' - Valid equilibrium solvers are "
f"{list(EQUILIBRIUM_SOLVER_REGISTRY.keys())}."
)
continue
solver(solar_cell[j], **options)
@register_action("short_circuit")
def solve_short_circuit(solar_cell: SolarCell, options: State):
"""Solves the electronic properfies of the cell at short circuit conditons.
The junction objects are updated with the bandstructure and recombination profiles.
Args:
solar_cell: The solar cell to solve.
options: Options required by the solver.
"""
solve_optics(solar_cell, options)
for j in solar_cell.junction_indices:
solver = SHORT_CIRCUIT_SOLVER_REGISTRY.get(solar_cell[j].kind, None)
if solver is None:
warn(
"ERROR in 'solve_short_circuit' - Valid short circuit solvers are "
f"{list(SHORT_CIRCUIT_SOLVER_REGISTRY.keys())}."
)
continue
solver(solar_cell[j], **options)
def prepare_solar_cell(solar_cell, options):
"""This function scans all the layers and junctions of the cell, calculating the relative position of each of them with respect the front surface (offset).
This information will later be use by the optical calculators, for example. It also processes the 'position' option, which determines the spacing used if the
solver is going to calculate depth-dependent absorption.
:param solar_cell: A solar_cell object
:param options: an options (State) object with user/default options
:return: None
"""
offset = 0
layer_widths = []
for j, layer_object in enumerate(solar_cell):
# Independent layers, for example in a AR coating
if type(layer_object) is Layer:
layer_widths.append(layer_object.width)
# Each Tunnel junctions can also have some layers with a given thickness.
elif type(layer_object) is TunnelJunction:
junction_width = 0
for i, layer in enumerate(layer_object):
junction_width += layer.width
layer_widths.append(layer.width)
solar_cell[j].width = junction_width
# For each junction, and layer within the junction, we get the layer width.
elif type(layer_object) is Junction:
try:
kind = solar_cell[j].kind
except AttributeError as err:
print(
"ERROR preparing the solar cell: Junction {} has no kind!".format(j)
)
raise err
# This junctions will not, typically, have a width
if kind in ["2D", "DB"]:
layer_widths.append(1e-6)
# 2D and DB junctions do not often have a width (or need it) so we set an arbitrary width
if not hasattr(layer_object, "width"):
solar_cell[j].width = 1e-6 # 1 µm
else:
junction_width = 0
for i, layer in enumerate(layer_object):
layer_widths.append(layer.width)
junction_width += layer.width
solar_cell[j].width = junction_width
solar_cell[j].offset = offset
offset += solar_cell[j].width
solar_cell.width = offset
process_position(solar_cell, options, layer_widths)
def process_position(solar_cell, options, layer_widths):
"""
To control the depth spacing, the user can pass:
- a vector which specifies each position (in m) at which the depth should be calculated
- a single number which specifies the spacing (in m) to generate the position vector, e.g. 1e-9 for 1 nm spacing
- a list of numbers which specify the spacing (in m) to be used in each layer. This list can have EITHER the length
of the number of individual layers + the number of junctions in the cell object, OR the length of the total number of individual layers including layers inside junctions.
:param solar_cell: a SolarCell object
:param options: aan options (State) object with user/default options
:param layer_widths: list of widths of the individual layers in the stack, treating the layers within junctions as individual layers
:return: None
"""
if options.position is None:
options.position = [max(1e-10, width / 5000) for width in layer_widths]
layer_offsets = np.insert(np.cumsum(layer_widths), 0, 0)
options.position = np.hstack(
[
np.arange(
layer_offsets[j],
layer_offsets[j] + layer_width,
options.position[j],
)
for j, layer_width in enumerate(layer_widths)
]
)
elif isinstance(options.position, int) or isinstance(options.position, float):
options.position = np.arange(0, solar_cell.width, options.position)
elif isinstance(options.position, list) or isinstance(options.position, np.ndarray):
if len(options.position) == 1:
options.position = np.arange(0, solar_cell.width, options.position[0])
if len(options.position) == len(solar_cell):
options.position = np.hstack(
[
np.arange(
layer_object.offset,
layer_object.offset + layer_object.width,
options.position[j],
)
for j, layer_object in enumerate(solar_cell)
]
)
elif len(options.position) == len(layer_widths):
layer_offsets = np.insert(np.cumsum(layer_widths), 0, 0)
options.position = np.hstack(
[
np.arange(
layer_offsets[j],
layer_offsets[j] + layer_width,
options.position[j],
)
for j, layer_width in enumerate(layer_widths)
]
)