/
tmm.py
executable file
·213 lines (176 loc) · 7.62 KB
/
tmm.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
import types
from typing import List, Optional
import numpy as np
from numpy.typing import NDArray
from ..absorption_calculator import (
OptiStack,
calculate_absorption_profile,
calculate_rat,
)
from ..registries import register_optics
from ..solar_cell import SolarCell
from ..structure import Junction, Layer, TunnelJunction
@register_optics(name="TMM")
def solve_tmm(
solar_cell: SolarCell,
wavelength: NDArray,
position: NDArray,
BL_correction: bool = True,
theta: float = 0.0,
pol: str = "u",
zero_threshold: float = 1e-5,
no_back_reflection: bool = True,
coherency_list: Optional[List[str]] = None,
**kwargs
) -> None:
"""Calculates the RAT of a solar cell object using the transfer matrix method.
Internally, it creates an OptiStack and then it calculates the optical properties of
the whole structure. A substrate can be specified in the SolarCell object, which is
treated as a semi-infinite transmission medium. Shading can also be specified (as a
fraction).
A coherency_list option can be provided:
Args:
solar_cell: A solar_cell object
wavelength: Array of wavelegth at which the optics are calculated.
position: Array of positions in the z direction to calculate the absorption vs
depth.
BL_correction: If is set to True, thick layers (thickness > 10*maximum
wavelength) are treated incoherently using the Beer-Lambert law, to avoid
the calculation of unphysical interference oscillations in the R/A/T
spectra.
theta: the polar incidence angle, in degrees, with 0 degrees being normal
incidence.
pol: the polarization of the light ('s', 'p' or 'u')
zero_threshold: when the fraction of incident light absorbed in a layer is
less than this value, the absorption profile is completely set to zero for
both coherent and incoherent calculations. This is applied on a
wavelength-by-wavelength basis and is intended to prevent errors where
integrating a weak absorption profile in a layer over many points leads to
calculated EQE > total absorption in that layer.
no_back_reflection: Sets whether reflections from the back surface are
suppressed (if set to True, the default), or taken into account (if set to
False).
coherency_list: If present, this should have the same number of elements than
number of layers (if a Junction contains multiple Layers, each should have
its own entry in the coherency list). Each element is either 'c' for
coherent treatment of that layer or 'i' for incoherent treatment.
Return:
None
"""
# We include the shadowing losses
initial = (1 - solar_cell.shading) if hasattr(solar_cell, "shading") else 1
# Now we calculate the absorbed and transmitted light. We first get all the relevant
# parameters from the objects
all_layers = []
widths = []
n_layers_junction = []
for j, layer_object in enumerate(solar_cell):
# Attenuation due to absorption in the AR coatings or any layer in the front
# that is not part of the junction
if type(layer_object) is Layer:
all_layers.append(layer_object)
widths.append(layer_object.width)
n_layers_junction.append(1)
# For each junction, and layer within the junction, we get the absorption
# coefficient and the layer width.
elif type(layer_object) in [TunnelJunction, Junction]:
n_layers_junction.append(len(layer_object))
for i, layer in enumerate(layer_object):
all_layers.append(layer)
widths.append(layer.width)
# With all the information, we create the optical stack
full_stack = OptiStack(
all_layers,
no_back_reflection=no_back_reflection,
substrate=solar_cell.substrate,
incidence=solar_cell.incidence,
)
if coherency_list is not None:
coherent = False
if len(coherency_list) != full_stack.num_layers:
raise ValueError(
"Error: The coherency list must have as many elements (now {}) as the "
"number of layers (now {}).".format(
len(coherency_list), full_stack.num_layers
)
)
else:
coherent = True
# assume it's safe to ignore interference effects
if BL_correction and any(widths > 10 * np.max(wavelength)):
make_incoherent = np.where(np.array(widths) > 10 * np.max(wavelength))[0]
print("Treating layer(s) " + str(make_incoherent).strip("[]") + " incoherently")
if not coherency_list:
coherency_list_ = np.array(len(all_layers) * ["c"])
coherent = False
else:
coherency_list_ = np.array(coherency_list)
coherency_list_[make_incoherent] = "i"
coherency_list = coherency_list_.tolist()
position = position * 1e9
profile_position = position[position < sum(full_stack.widths)]
print("Calculating RAT...")
RAT = calculate_rat(
full_stack,
wavelength * 1e9,
angle=theta,
coherent=coherent,
coherency_list=coherency_list,
no_back_reflection=no_back_reflection,
pol=pol,
)
print("Calculating absorption profile...")
out = calculate_absorption_profile(
full_stack,
wavelength * 1e9,
dist=profile_position,
angle=theta,
no_back_reflection=no_back_reflection,
pol=pol,
coherent=coherent,
coherency_list=coherency_list,
zero_threshold=zero_threshold,
RAT_out=RAT,
)
# With all this information, we are ready to calculate the differential absorption
# function
diff_absorption, all_absorbed = calculate_absorption_tmm(out, initial)
# Each building block (layer or junction) needs to have access to the absorbed light
# in its region. We update each object with that information.
# first entry is R, last entry is T
layer = 0
A_per_layer = np.array(RAT["A_per_layer"][1:-1])
for j in range(len(solar_cell)):
solar_cell[j].layer_absorption = initial * np.sum(
A_per_layer[layer : (layer + n_layers_junction[j])], axis=0
)
solar_cell[j].diff_absorption = diff_absorption
solar_cell[j].absorbed = types.MethodType(absorbed, solar_cell[j])
layer = layer + n_layers_junction[j]
solar_cell.reflected = RAT["R"] * initial
solar_cell.absorbed = sum(
[solar_cell[x].layer_absorption for x in np.arange(len(solar_cell))]
)
solar_cell.transmitted = initial - solar_cell.reflected - solar_cell.absorbed
def absorbed(self, z):
out = self.diff_absorption(self.offset + z) * (z < self.width)
return out.T
def calculate_absorption_tmm(tmm_out, initial=1):
all_z = tmm_out["position"] * 1e-9
all_abs = initial * tmm_out["absorption"] / 1e-9
def diff_absorption(z):
idx = all_z.searchsorted(z)
idx = np.where(idx <= len(all_z) - 1, idx, len(all_z) - 1)
idx = np.where(idx > 0, idx, 1)
try:
z1 = all_z[idx - 1]
z2 = all_z[idx]
f = np.divide(z - z1, z2 - z1,
out=np.zeros_like(z), where=np.abs(z2-z1) > 1e-12)
# this is to avoid divide by zero errors (|f| gets very large) when z1 = z2
out = (1-f) * all_abs[:, idx - 1] + f * all_abs[:, idx]
except IndexError:
out = all_abs[:, idx]
return out
all_absorbed = np.trapz(diff_absorption(all_z), all_z)
return diff_absorption, all_absorbed