scambio.absorbance
Solar cell absorbance.
1r"""Solar cell absorbance.""" 2 3import numpy as np 4import seaborn as sns 5from collections.abc import Mapping, Sequence 6from matplotlib import pyplot as plt 7from numbers import Real 8from typing import Any, Self 9from scambio.represent import repr_poly 10from scambio.special import rectangle 11 12 13sns.set_theme( 14 context="talk", 15 style="white", 16 rc={"figure.titlesize": "medium", "axes.formatter.useoffset": False}, 17) 18 19 20class SpectralAbsorbancePPoly: 21 r"""Spectral absorbance piecewise polynomial. 22 23 Note: 24 Spectral absorbance is expressed as a $m$-th degree piecewise 25 polynomial of the photon energy [$\mathrm{eV}$], specified through: 26 27 - photon energy partition (1d-array): 28 29 $$ 30 \begin{bmatrix} 31 \epsilon_{0} & \epsilon_{1} & \dots & \epsilon_{n + 1} 32 \end{bmatrix} 33 $$ 34 35 with $\epsilon_{i} < \epsilon_{i + 1}$. 36 37 - polynomial coefficients for each partition interval (2d-array): 38 39 $$ 40 \begin{bmatrix} 41 p_{0, 0} & p_{0, 1} & \dots & p_{0, n} \\\ 42 p_{1, 0} & p_{1, 1} & \dots & p_{1, n} \\\ 43 \dots & & & \\\ 44 p_{m, 0} & p_{m, 1} & \dots & p_{m, n} 45 \end{bmatrix} 46 $$ 47 48 so that: 49 50 $$ 51 A(\epsilon) = p_{0, i} + p_{1, i} \, \epsilon + \dots \ + p_{m, i} \, \epsilon^{m} 52 $$ 53 54 for $\epsilon_{i} \le \epsilon \lt \epsilon_{i + 1}$ 55 56 Args: 57 energy: Photon energy partition [$\mathrm{eV}$]. 58 pcoeff: Polynomial coefficients 59 [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$]. 60 """ 61 62 def __init__( 63 self, 64 energy: Sequence[Real, ...], 65 pcoeff: Sequence[Sequence[Real, ...], ...], 66 ) -> None: 67 r"""Initialize SpectralAbsorbancePPoly object.""" 68 self.set_energy_and_pcoeff(energy, pcoeff) 69 70 def __repr__(self) -> str: 71 r"""Represent SpectralAbsorbancePPoly object.""" 72 rows = ["Spectral absorbance:"] 73 for i, pcoeff_i in enumerate(self.pcoeff.T): 74 rows.append( 75 "-" 76 + " " 77 + "[" 78 + f"{self.energy[i]:.2e}" 79 + ", " 80 + f"{self.energy[i + 1]:.2e}" 81 + "]" 82 + " " 83 + "eV" 84 + " --> " 85 + f"{repr_poly(pcoeff_i)}" 86 ) 87 return "\n".join(rows) 88 89 @property 90 def energy(self) -> np.ndarray[float]: 91 r"""Photon energy partition [$\mathrm{eV}$].""" 92 return self._energy 93 94 @property 95 def pcoeff(self) -> np.ndarray[float]: 96 r"""Polynomial coefficients [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$].""" 97 return self._pcoeff 98 99 @property 100 def onset(self) -> float: 101 r"""Absorption onset [$\mathrm{eV}$].""" 102 if self._onset is None: 103 self.set_onset_and_offset() 104 return self._onset 105 106 @property 107 def offset(self) -> float: 108 r"""Absorption offset [$\mathrm{eV}$].""" 109 if self._offset is None: 110 self.set_onset_and_offset() 111 return self._offset 112 113 def clear_cached_properties(self) -> None: 114 r"""Clear cached properties.""" 115 self._onset = None 116 self._offset = None 117 118 def set_energy_and_pcoeff( 119 self, 120 energy: Sequence[Real, ...], 121 pcoeff: Sequence[Sequence[Real, ...], ...], 122 ) -> None: 123 r"""Set photon energy partition and polynomial coefficients. 124 125 Args: 126 energy: Photon energy partition [$\mathrm{eV}$]. 127 pcoeff: Polynomial coefficients 128 [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$]. 129 130 Raises: 131 ValueError: If photon energy partition is not valid, or piecewise 132 polynomial coefficients do not form a 2d-array or are not 133 commensurate with photon energy partition. 134 """ 135 energy = np.asarray(energy, dtype=float) 136 if not np.array_equal(energy, np.unique(energy)): 137 raise ValueError("Invalid photon energy partition!") 138 139 pcoeff = np.asarray(pcoeff, dtype=float) 140 if not pcoeff.ndim == 2: 141 raise ValueError( 142 "Piecewise polynomial coefficients " + "do not form a 2d-array!" 143 ) 144 if not pcoeff.shape[1] == energy.size - 1: 145 raise ValueError( 146 "Piecewise polynomial coefficients " 147 + "are not commensurate with photon energy partition!" 148 ) 149 150 self._energy = energy 151 self._pcoeff = pcoeff 152 self.clear_cached_properties() 153 154 def set_onset_and_offset(self) -> None: 155 r"""Set absorption onset and offset.""" 156 has_nonzero_absorp = self.pcoeff.any(axis=0) 157 nonzero_absorp_i = np.argwhere(has_nonzero_absorp).flatten() 158 if not nonzero_absorp_i.size > 0: 159 self._onset = np.nan 160 self._offset = np.nan 161 else: 162 onset_i = nonzero_absorp_i[0] 163 offset_i = nonzero_absorp_i[-1] + 1 164 self._onset = self.energy[onset_i] 165 self._offset = self.energy[offset_i] 166 167 def eval_sp_absorb(self, energy: Sequence[Real, ...]) -> np.ndarray[float]: 168 r"""Evaluate spectral absorbance at given photon energy values. 169 170 Args: 171 energy: Photon energy values [$\mathrm{eV}$]. 172 173 Returns: 174 Spectral absorbance values. 175 """ 176 energy = np.asarray(energy, dtype=float).flatten() 177 sp_absorb = np.array([], dtype=float) 178 for e in energy: 179 if e < np.amin(self.energy) or e >= np.amax(self.energy): 180 sp_absorb = np.append(sp_absorb, np.nan) 181 else: 182 i = np.searchsorted(self.energy, e, side="right") 183 pcoeff = self.pcoeff[:, i - 1] 184 sp_absorb = np.append( 185 sp_absorb, 186 np.polynomial.polynomial.polyval(e, pcoeff), 187 ) 188 return sp_absorb 189 190 def plot_sp_absorb_vs_energy( 191 self, 192 *, 193 energy_min: Real, 194 energy_max: Real, 195 energy_inc: Real, 196 ax: plt.Axes | None = None, 197 ) -> plt.Figure | None: 198 r"""Plot spectral absorbance vs photon energy. 199 200 Args: 201 energy_min: Photon energy minimum [$\mathrm{eV}$]. 202 energy_max: Photon energy maximum [$\mathrm{eV}$]. 203 energy_inc: Photon energy increment [$\mathrm{eV}$]. 204 ax: Plot axes. If None, new figure and axes are created. 205 206 Returns: 207 Plot figure. 208 """ 209 if not 0.0 <= energy_min < energy_max < np.inf: 210 print("Photon energy range is not valid!") 211 return None 212 213 if not 0.0 < energy_inc < energy_max - energy_min: 214 print("Photon energy increment is not valid!") 215 return None 216 217 energy = np.arange(energy_min, energy_max + energy_inc, energy_inc) 218 sp_absorb = self.eval_sp_absorb(energy) 219 220 if ax is None: 221 fig, ax = plt.subplots(tight_layout=True) 222 ax.set_xlabel(r"Photon energy / eV") 223 ax.set_ylabel(r"Spectral absorbance") 224 ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 225 else: 226 fig = ax.get_figure() 227 228 ax.plot(energy, sp_absorb, color="k") 229 ax.fill_between(energy, sp_absorb, alpha=0.5) 230 return fig 231 232 @classmethod 233 def from_data( 234 cls, 235 energy: Sequence[Real, ...], 236 sp_absorb: Sequence[Real, ...], 237 ) -> Self: 238 r"""Create SpectralAbsorbancePPoly from data. 239 240 Args: 241 energy: Photon energy values [$\mathrm{eV}$]. 242 sp_absorb: Spectral absorbance values. 243 244 Returns: 245 Spectral absorbance piecewise polynomial. 246 247 Raises: 248 ValueError: If there are duplicate photon energy values. 249 """ 250 energy = np.asarray(energy, dtype=float).flatten() 251 sp_absorb = np.asarray(sp_absorb, dtype=float).flatten() 252 253 sort_i = np.argsort(energy) 254 energy = energy[sort_i] 255 sp_absorb = sp_absorb[sort_i] 256 sp_absorb[np.isclose(sp_absorb, 0.0)] = 0.0 257 258 if not np.array_equal(energy, np.unique(energy)): 259 raise ValueError("Duplicate photon energy values!") 260 261 slope = np.diff(sp_absorb) / np.diff(energy) 262 offset = sp_absorb[:-1] - slope * energy[:-1] 263 pcoeff = np.asarray([offset, slope]) 264 265 return cls(energy, pcoeff) 266 267 268class ResolvedSpectralAbsorbancePPoly: 269 r"""Resolved spectral absorbance piecewise polynomials. 270 271 Args: 272 ii: Intermediate-intermediate spectral absorbance. 273 vi: Valence-intermediate spectral absorbance. 274 ic: Intermediate-conduction spectral absorbance. 275 vc: Valence-conduction spectral absorbance. 276 """ 277 278 def __init__( 279 self, 280 *, 281 ii: SpectralAbsorbancePPoly | Any = None, 282 vi: SpectralAbsorbancePPoly | Any = None, 283 ic: SpectralAbsorbancePPoly | Any = None, 284 vc: SpectralAbsorbancePPoly | Any = None, 285 ) -> None: 286 r"""Initialize ResolvedSpectralAbsorbancePPoly object.""" 287 self.set_rsv_sp_absorb_ppoly(ii=ii, vi=vi, ic=ic, vc=vc) 288 289 def __repr__(self) -> str: 290 r"""Represent ResolvedSpectralAbsorbancePPoly object.""" 291 blocks = [] 292 if self.ii is not None: 293 blocks.append(f"{self.ii}".replace(":", " (ii):")) 294 if self.vi is not None: 295 blocks.append(f"{self.vi}".replace(":", " (vi):")) 296 if self.ic is not None: 297 blocks.append(f"{self.ic}".replace(":", " (ic):")) 298 if self.vc is not None: 299 blocks.append(f"{self.vc}".replace(":", " (vc):")) 300 return "\n\n".join(blocks) 301 302 @property 303 def ii(self) -> SpectralAbsorbancePPoly | None: 304 r"""Intermediate-intermediate spectral absorbance.""" 305 return self._ii 306 307 @property 308 def vi(self) -> SpectralAbsorbancePPoly | None: 309 r"""Valence-intermediate spectral absorbance.""" 310 return self._vi 311 312 @property 313 def ic(self) -> SpectralAbsorbancePPoly | None: 314 r"""Intermediate-conduction spectral absorbance.""" 315 return self._ic 316 317 @property 318 def vc(self) -> SpectralAbsorbancePPoly | None: 319 r"""Valence-conduction spectral absorbance.""" 320 return self._vc 321 322 def clear_cached_properties(self) -> None: 323 r"""Clear cached properties.""" 324 325 def set_rsv_sp_absorb_ppoly( 326 self, 327 *, 328 ii: SpectralAbsorbancePPoly | Any = None, 329 vi: SpectralAbsorbancePPoly | Any = None, 330 ic: SpectralAbsorbancePPoly | Any = None, 331 vc: SpectralAbsorbancePPoly | Any = None, 332 ) -> None: 333 r"""Set resolved spectral absorbance piecewise polynomials. 334 335 Args: 336 ii: Intermediate-intermediate spectral absorbance. 337 vi: Valence-intermediate spectral absorbance. 338 ic: Intermediate-conduction spectral absorbance. 339 vc: Valence-conduction spectral absorbance. 340 341 Raises: 342 ValueError: If spectral absorbance piecewise polynomials do not 343 have the same photon energy partition or degree. 344 """ 345 energy_xy_seq = [] 346 pcoeff_xy_seq = [] 347 for xy in [ii, vi, ic, vc]: 348 if isinstance(xy, SpectralAbsorbancePPoly): 349 energy_xy_seq.append(xy.energy) 350 pcoeff_xy_seq.append(xy.pcoeff) 351 352 for energy_xy in energy_xy_seq: 353 if not np.array_equal(energy_xy, energy_xy_seq[0]): 354 raise ValueError( 355 "Spectral absorbance piecewise polynomials " 356 + "must have the same photon energy partition!" 357 ) 358 359 for pcoeff_xy in pcoeff_xy_seq: 360 if not pcoeff_xy.shape == pcoeff_xy_seq[0].shape: 361 raise ValueError( 362 "Spectral absorbance piecewise polynomials " 363 + "must have the same degree!" 364 ) 365 366 self._ii = ii if isinstance(ii, SpectralAbsorbancePPoly) else None 367 self._vi = vi if isinstance(vi, SpectralAbsorbancePPoly) else None 368 self._ic = ic if isinstance(ic, SpectralAbsorbancePPoly) else None 369 self._vc = vc if isinstance(vc, SpectralAbsorbancePPoly) else None 370 self.clear_cached_properties() 371 372 def plot_rsv_sp_absorb_vs_energy( 373 self, 374 *, 375 energy_min: Real, 376 energy_max: Real, 377 energy_inc: Real, 378 ax: plt.Axes | None = None, 379 ) -> plt.Figure | None: 380 r"""Plot resolved spectral absorbance vs photon energy. 381 382 Args: 383 energy_min: Photon energy minimum [$\mathrm{eV}$]. 384 energy_max: Photon energy maximum [$\mathrm{eV}$]. 385 energy_inc: Photon energy increment [$\mathrm{eV}$]. 386 ax: Plot axes. If None, new figure and axes are created. 387 388 Returns: 389 Plot figure. 390 """ 391 if not 0.0 <= energy_min < energy_max < np.inf: 392 print("Photon energy range is not valid!") 393 return None 394 395 if not 0.0 < energy_inc < energy_max - energy_min: 396 print("Photon energy increment is not valid!") 397 return None 398 399 energy = np.arange(energy_min, energy_max + energy_inc, energy_inc) 400 401 rsv_sp_absorb = {} 402 for xy in ["ii", "vi", "ic", "vc"]: 403 sp_absorb_xy_ppoly = getattr(self, xy) 404 if sp_absorb_xy_ppoly is not None: 405 sp_absorb_xy = sp_absorb_xy_ppoly.eval_sp_absorb(energy) 406 else: 407 sp_absorb_xy = 0.0 * energy 408 rsv_sp_absorb.update({xy: sp_absorb_xy}) 409 410 sp_absorb_tot = np.sum( 411 [sp_absorb_xy for sp_absorb_xy in rsv_sp_absorb.values()], 412 axis=0, 413 ) 414 415 if ax is None: 416 fig, ax = plt.subplots(tight_layout=True) 417 ax.set_xlabel(r"Photon energy / $\mathrm{eV}$") 418 ax.set_ylabel(r"Spectral absorbance") 419 ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 420 else: 421 fig = ax.get_figure() 422 423 ax.plot(energy, sp_absorb_tot, color="k") 424 ax.stackplot( 425 energy, 426 rsv_sp_absorb.values(), 427 labels=rsv_sp_absorb.keys(), 428 ) 429 ax.legend( 430 loc="lower center", 431 bbox_to_anchor=[0.5, 1.0], 432 frameon=False, 433 ncols=len(rsv_sp_absorb), 434 ) 435 return fig 436 437 @classmethod 438 def sq1961( 439 cls, 440 *, 441 bandgap_vc: Real, 442 bandwidth_vc: Real, 443 ) -> Self | None: 444 r"""Shockley-Queisser resolved spectral absorbance piecewise polynomials. 445 446 Note: 447 See [original article](https://dx.doi.org/10.1063%2F1.1736034) for 448 details. 449 450 Args: 451 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 452 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 453 454 Returns: 455 Resolved spectral absorbance piecewise polynomial. 456 """ 457 if not 0.0 < bandgap_vc < np.inf: 458 print("Valence-conduction optical band gap must be finite and > 0!") 459 return None 460 461 if not 0.0 <= bandwidth_vc <= np.inf: 462 print("Valence-conduction optical band width must be nonnegative!") 463 return None 464 465 onset = bandgap_vc 466 offset = bandgap_vc + bandwidth_vc 467 energy = np.unique([0.0, onset, offset, np.inf]).astype(float) 468 469 energy_mid = 0.5 * (energy[:-1] + energy[1:]) 470 sp_absorp = rectangle(energy_mid, a=onset, b=offset) 471 pcoeff = sp_absorp.reshape(1, -1) 472 473 sp_absorb_ppoly = SpectralAbsorbancePPoly(energy, pcoeff) 474 return cls(vc=sp_absorb_ppoly) 475 476 @classmethod 477 def lh2008( 478 cls, 479 *, 480 bandgap_vi: Real, 481 bandgap_ic: Real, 482 bandgap_vc: Real, 483 bandwidth_ii: Real, 484 bandwidth_vi: Real, 485 bandwidth_ic: Real, 486 bandwidth_vc: Real, 487 variant: str, 488 ) -> Self | None: 489 r"""Levi-Honsberg resolved spectral absorbance piecewise polynomials. 490 491 Note: 492 See 493 [original article](http://dx.doi.org/10.1103%2FPhysRevB.78.165122) 494 for details. 495 496 Args: 497 bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$]. 498 bandgap_ic: Intermediate-conduction optical band gap 499 [$\mathrm{eV}$]. 500 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 501 bandwidth_ii: Intermediate-intermediate optical band width 502 [$\mathrm{eV}$]. 503 bandwidth_vi: Valence-intermediate optical band width 504 [$\mathrm{eV}$]. 505 bandwidth_ic: Intermediate-conduction optical band width 506 [$\mathrm{eV}$]. 507 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 508 variant: Model variant ('equal', 'inter', or 'intra'). 509 510 Returns: 511 Resolved spectral absorbance piecewise polynomials. 512 """ 513 if not ( 514 0.0 < bandgap_vi < np.inf 515 and 0.0 < bandgap_ic < np.inf 516 and 0.0 < bandgap_vc < np.inf 517 ): 518 print("Optical band gaps must be finite and positive!") 519 return None 520 521 if not ( 522 bandwidth_ii >= 0.0 523 and bandwidth_vi >= 0.0 524 and bandwidth_ic >= 0.0 525 and bandwidth_vc >= 0.0 526 ): 527 print("Optical band widths must be nonnegative!") 528 return None 529 530 if not bandwidth_ii < np.inf: 531 print("Intermediate band width must be finite!") 532 return None 533 534 if not variant in {"equal", "inter", "intra"}: 535 print("Variant must be 'equal', 'inter' or 'intra'!") 536 return None 537 538 if not bandgap_vi + bandgap_ic + bandwidth_ii <= bandgap_vc: 539 if np.isclose(bandgap_vi + bandgap_ic + bandwidth_ii, bandgap_vc): 540 # print("Removing floating-point representation error...") 541 bandgap_vc = bandgap_vi + bandgap_ic + bandwidth_ii 542 else: 543 print("Valence-conduction optical bandgap is too small!") 544 return None 545 546 if variant == "inter": 547 onset = [0.0] 548 offset = [min(bandwidth_ii, bandgap_vi, bandgap_ic)] 549 else: 550 onset = [0.0] 551 offset = [bandwidth_ii] 552 553 if variant == "intra": 554 onset += [ 555 max(bandgap_vi, bandwidth_ii), 556 max(bandgap_ic, bandwidth_ii), 557 max(bandgap_vc, bandwidth_ii), 558 ] 559 offset += [ 560 max(bandgap_vi + bandwidth_vi, bandwidth_ii), 561 max(bandgap_ic + bandwidth_ic, bandwidth_ii), 562 max(bandgap_vc + bandwidth_vc, bandwidth_ii), 563 ] 564 else: 565 onset += [bandgap_vi, bandgap_ic, bandgap_vc] 566 offset += [ 567 bandgap_vi + bandwidth_vi, 568 bandgap_ic + bandwidth_ic, 569 bandgap_vc + bandwidth_vc, 570 ] 571 572 energy = np.unique([0.0, np.inf] + onset + offset).astype(float) 573 574 energy_mid = 0.5 * (energy[:-1] + energy[1:]) 575 sp_absorp = rectangle(energy_mid, a=onset, b=offset) 576 sp_absorp_tot = np.sum(sp_absorp, axis=0) 577 pcoeff = sp_absorp / np.where(sp_absorp_tot == 0.0, 1.0, sp_absorp_tot) 578 579 rsv_sp_absorb_ppoly = { 580 xy: SpectralAbsorbancePPoly(energy, pcoeff_xy.reshape(1, -1)) 581 for xy, pcoeff_xy in zip(["ii", "vi", "ic", "vc"], pcoeff) 582 } 583 return cls(**rsv_sp_absorb_ppoly) 584 585 @classmethod 586 def from_data( 587 cls, 588 energy: Sequence[Real, ...], 589 rsv_sp_absorb: Mapping[str, Sequence[Real, ...]], 590 ) -> Self: 591 r"""Create resolved spectral absorbance piecewise polynomials from data. 592 593 Args: 594 energy: Photon energy values [$\mathrm{eV}$]. 595 rsv_sp_absorb: Mapping of electronic transition labels ('ii', 'vi', 596 'ic', 'vc') into corresponding spectral absorbance values. 597 598 Returns: 599 Resolved spectral absorbace piecewise polynomials. 600 """ 601 rsv_sp_absorb = { 602 xy: SpectralAbsorbancePPoly.from_data(energy, sp_absorb_xy) 603 for xy, sp_absorb_xy in rsv_sp_absorb.items() 604 if sp_absorb_xy is not None 605 } 606 return cls(**rsv_sp_absorb)
21class SpectralAbsorbancePPoly: 22 r"""Spectral absorbance piecewise polynomial. 23 24 Note: 25 Spectral absorbance is expressed as a $m$-th degree piecewise 26 polynomial of the photon energy [$\mathrm{eV}$], specified through: 27 28 - photon energy partition (1d-array): 29 30 $$ 31 \begin{bmatrix} 32 \epsilon_{0} & \epsilon_{1} & \dots & \epsilon_{n + 1} 33 \end{bmatrix} 34 $$ 35 36 with $\epsilon_{i} < \epsilon_{i + 1}$. 37 38 - polynomial coefficients for each partition interval (2d-array): 39 40 $$ 41 \begin{bmatrix} 42 p_{0, 0} & p_{0, 1} & \dots & p_{0, n} \\\ 43 p_{1, 0} & p_{1, 1} & \dots & p_{1, n} \\\ 44 \dots & & & \\\ 45 p_{m, 0} & p_{m, 1} & \dots & p_{m, n} 46 \end{bmatrix} 47 $$ 48 49 so that: 50 51 $$ 52 A(\epsilon) = p_{0, i} + p_{1, i} \, \epsilon + \dots \ + p_{m, i} \, \epsilon^{m} 53 $$ 54 55 for $\epsilon_{i} \le \epsilon \lt \epsilon_{i + 1}$ 56 57 Args: 58 energy: Photon energy partition [$\mathrm{eV}$]. 59 pcoeff: Polynomial coefficients 60 [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$]. 61 """ 62 63 def __init__( 64 self, 65 energy: Sequence[Real, ...], 66 pcoeff: Sequence[Sequence[Real, ...], ...], 67 ) -> None: 68 r"""Initialize SpectralAbsorbancePPoly object.""" 69 self.set_energy_and_pcoeff(energy, pcoeff) 70 71 def __repr__(self) -> str: 72 r"""Represent SpectralAbsorbancePPoly object.""" 73 rows = ["Spectral absorbance:"] 74 for i, pcoeff_i in enumerate(self.pcoeff.T): 75 rows.append( 76 "-" 77 + " " 78 + "[" 79 + f"{self.energy[i]:.2e}" 80 + ", " 81 + f"{self.energy[i + 1]:.2e}" 82 + "]" 83 + " " 84 + "eV" 85 + " --> " 86 + f"{repr_poly(pcoeff_i)}" 87 ) 88 return "\n".join(rows) 89 90 @property 91 def energy(self) -> np.ndarray[float]: 92 r"""Photon energy partition [$\mathrm{eV}$].""" 93 return self._energy 94 95 @property 96 def pcoeff(self) -> np.ndarray[float]: 97 r"""Polynomial coefficients [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$].""" 98 return self._pcoeff 99 100 @property 101 def onset(self) -> float: 102 r"""Absorption onset [$\mathrm{eV}$].""" 103 if self._onset is None: 104 self.set_onset_and_offset() 105 return self._onset 106 107 @property 108 def offset(self) -> float: 109 r"""Absorption offset [$\mathrm{eV}$].""" 110 if self._offset is None: 111 self.set_onset_and_offset() 112 return self._offset 113 114 def clear_cached_properties(self) -> None: 115 r"""Clear cached properties.""" 116 self._onset = None 117 self._offset = None 118 119 def set_energy_and_pcoeff( 120 self, 121 energy: Sequence[Real, ...], 122 pcoeff: Sequence[Sequence[Real, ...], ...], 123 ) -> None: 124 r"""Set photon energy partition and polynomial coefficients. 125 126 Args: 127 energy: Photon energy partition [$\mathrm{eV}$]. 128 pcoeff: Polynomial coefficients 129 [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$]. 130 131 Raises: 132 ValueError: If photon energy partition is not valid, or piecewise 133 polynomial coefficients do not form a 2d-array or are not 134 commensurate with photon energy partition. 135 """ 136 energy = np.asarray(energy, dtype=float) 137 if not np.array_equal(energy, np.unique(energy)): 138 raise ValueError("Invalid photon energy partition!") 139 140 pcoeff = np.asarray(pcoeff, dtype=float) 141 if not pcoeff.ndim == 2: 142 raise ValueError( 143 "Piecewise polynomial coefficients " + "do not form a 2d-array!" 144 ) 145 if not pcoeff.shape[1] == energy.size - 1: 146 raise ValueError( 147 "Piecewise polynomial coefficients " 148 + "are not commensurate with photon energy partition!" 149 ) 150 151 self._energy = energy 152 self._pcoeff = pcoeff 153 self.clear_cached_properties() 154 155 def set_onset_and_offset(self) -> None: 156 r"""Set absorption onset and offset.""" 157 has_nonzero_absorp = self.pcoeff.any(axis=0) 158 nonzero_absorp_i = np.argwhere(has_nonzero_absorp).flatten() 159 if not nonzero_absorp_i.size > 0: 160 self._onset = np.nan 161 self._offset = np.nan 162 else: 163 onset_i = nonzero_absorp_i[0] 164 offset_i = nonzero_absorp_i[-1] + 1 165 self._onset = self.energy[onset_i] 166 self._offset = self.energy[offset_i] 167 168 def eval_sp_absorb(self, energy: Sequence[Real, ...]) -> np.ndarray[float]: 169 r"""Evaluate spectral absorbance at given photon energy values. 170 171 Args: 172 energy: Photon energy values [$\mathrm{eV}$]. 173 174 Returns: 175 Spectral absorbance values. 176 """ 177 energy = np.asarray(energy, dtype=float).flatten() 178 sp_absorb = np.array([], dtype=float) 179 for e in energy: 180 if e < np.amin(self.energy) or e >= np.amax(self.energy): 181 sp_absorb = np.append(sp_absorb, np.nan) 182 else: 183 i = np.searchsorted(self.energy, e, side="right") 184 pcoeff = self.pcoeff[:, i - 1] 185 sp_absorb = np.append( 186 sp_absorb, 187 np.polynomial.polynomial.polyval(e, pcoeff), 188 ) 189 return sp_absorb 190 191 def plot_sp_absorb_vs_energy( 192 self, 193 *, 194 energy_min: Real, 195 energy_max: Real, 196 energy_inc: Real, 197 ax: plt.Axes | None = None, 198 ) -> plt.Figure | None: 199 r"""Plot spectral absorbance vs photon energy. 200 201 Args: 202 energy_min: Photon energy minimum [$\mathrm{eV}$]. 203 energy_max: Photon energy maximum [$\mathrm{eV}$]. 204 energy_inc: Photon energy increment [$\mathrm{eV}$]. 205 ax: Plot axes. If None, new figure and axes are created. 206 207 Returns: 208 Plot figure. 209 """ 210 if not 0.0 <= energy_min < energy_max < np.inf: 211 print("Photon energy range is not valid!") 212 return None 213 214 if not 0.0 < energy_inc < energy_max - energy_min: 215 print("Photon energy increment is not valid!") 216 return None 217 218 energy = np.arange(energy_min, energy_max + energy_inc, energy_inc) 219 sp_absorb = self.eval_sp_absorb(energy) 220 221 if ax is None: 222 fig, ax = plt.subplots(tight_layout=True) 223 ax.set_xlabel(r"Photon energy / eV") 224 ax.set_ylabel(r"Spectral absorbance") 225 ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 226 else: 227 fig = ax.get_figure() 228 229 ax.plot(energy, sp_absorb, color="k") 230 ax.fill_between(energy, sp_absorb, alpha=0.5) 231 return fig 232 233 @classmethod 234 def from_data( 235 cls, 236 energy: Sequence[Real, ...], 237 sp_absorb: Sequence[Real, ...], 238 ) -> Self: 239 r"""Create SpectralAbsorbancePPoly from data. 240 241 Args: 242 energy: Photon energy values [$\mathrm{eV}$]. 243 sp_absorb: Spectral absorbance values. 244 245 Returns: 246 Spectral absorbance piecewise polynomial. 247 248 Raises: 249 ValueError: If there are duplicate photon energy values. 250 """ 251 energy = np.asarray(energy, dtype=float).flatten() 252 sp_absorb = np.asarray(sp_absorb, dtype=float).flatten() 253 254 sort_i = np.argsort(energy) 255 energy = energy[sort_i] 256 sp_absorb = sp_absorb[sort_i] 257 sp_absorb[np.isclose(sp_absorb, 0.0)] = 0.0 258 259 if not np.array_equal(energy, np.unique(energy)): 260 raise ValueError("Duplicate photon energy values!") 261 262 slope = np.diff(sp_absorb) / np.diff(energy) 263 offset = sp_absorb[:-1] - slope * energy[:-1] 264 pcoeff = np.asarray([offset, slope]) 265 266 return cls(energy, pcoeff)
Spectral absorbance piecewise polynomial.
Note:
Spectral absorbance is expressed as a $m$-th degree piecewise polynomial of the photon energy [$\mathrm{eV}$], specified through:
photon energy partition (1d-array):
$$ \begin{bmatrix} \epsilon_{0} & \epsilon_{1} & \dots & \epsilon_{n + 1} \end{bmatrix} $$
with $\epsilon_{i} < \epsilon_{i + 1}$.
polynomial coefficients for each partition interval (2d-array):
$$ \begin{bmatrix} p_{0, 0} & p_{0, 1} & \dots & p_{0, n} \\ p_{1, 0} & p_{1, 1} & \dots & p_{1, n} \\ \dots & & & \\ p_{m, 0} & p_{m, 1} & \dots & p_{m, n} \end{bmatrix} $$
so that:
$$ A(\epsilon) = p_{0, i} + p_{1, i} \, \epsilon + \dots \ + p_{m, i} \, \epsilon^{m} $$
for $\epsilon_{i} \le \epsilon \lt \epsilon_{i + 1}$
Arguments:
- energy: Photon energy partition [$\mathrm{eV}$].
- pcoeff: Polynomial coefficients [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$].
63 def __init__( 64 self, 65 energy: Sequence[Real, ...], 66 pcoeff: Sequence[Sequence[Real, ...], ...], 67 ) -> None: 68 r"""Initialize SpectralAbsorbancePPoly object.""" 69 self.set_energy_and_pcoeff(energy, pcoeff)
Initialize SpectralAbsorbancePPoly object.
90 @property 91 def energy(self) -> np.ndarray[float]: 92 r"""Photon energy partition [$\mathrm{eV}$].""" 93 return self._energy
Photon energy partition [$\mathrm{eV}$].
95 @property 96 def pcoeff(self) -> np.ndarray[float]: 97 r"""Polynomial coefficients [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$].""" 98 return self._pcoeff
Polynomial coefficients [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$].
100 @property 101 def onset(self) -> float: 102 r"""Absorption onset [$\mathrm{eV}$].""" 103 if self._onset is None: 104 self.set_onset_and_offset() 105 return self._onset
Absorption onset [$\mathrm{eV}$].
107 @property 108 def offset(self) -> float: 109 r"""Absorption offset [$\mathrm{eV}$].""" 110 if self._offset is None: 111 self.set_onset_and_offset() 112 return self._offset
Absorption offset [$\mathrm{eV}$].
114 def clear_cached_properties(self) -> None: 115 r"""Clear cached properties.""" 116 self._onset = None 117 self._offset = None
Clear cached properties.
119 def set_energy_and_pcoeff( 120 self, 121 energy: Sequence[Real, ...], 122 pcoeff: Sequence[Sequence[Real, ...], ...], 123 ) -> None: 124 r"""Set photon energy partition and polynomial coefficients. 125 126 Args: 127 energy: Photon energy partition [$\mathrm{eV}$]. 128 pcoeff: Polynomial coefficients 129 [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$]. 130 131 Raises: 132 ValueError: If photon energy partition is not valid, or piecewise 133 polynomial coefficients do not form a 2d-array or are not 134 commensurate with photon energy partition. 135 """ 136 energy = np.asarray(energy, dtype=float) 137 if not np.array_equal(energy, np.unique(energy)): 138 raise ValueError("Invalid photon energy partition!") 139 140 pcoeff = np.asarray(pcoeff, dtype=float) 141 if not pcoeff.ndim == 2: 142 raise ValueError( 143 "Piecewise polynomial coefficients " + "do not form a 2d-array!" 144 ) 145 if not pcoeff.shape[1] == energy.size - 1: 146 raise ValueError( 147 "Piecewise polynomial coefficients " 148 + "are not commensurate with photon energy partition!" 149 ) 150 151 self._energy = energy 152 self._pcoeff = pcoeff 153 self.clear_cached_properties()
Set photon energy partition and polynomial coefficients.
Arguments:
- energy: Photon energy partition [$\mathrm{eV}$].
- pcoeff: Polynomial coefficients [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$].
Raises:
- ValueError: If photon energy partition is not valid, or piecewise polynomial coefficients do not form a 2d-array or are not commensurate with photon energy partition.
155 def set_onset_and_offset(self) -> None: 156 r"""Set absorption onset and offset.""" 157 has_nonzero_absorp = self.pcoeff.any(axis=0) 158 nonzero_absorp_i = np.argwhere(has_nonzero_absorp).flatten() 159 if not nonzero_absorp_i.size > 0: 160 self._onset = np.nan 161 self._offset = np.nan 162 else: 163 onset_i = nonzero_absorp_i[0] 164 offset_i = nonzero_absorp_i[-1] + 1 165 self._onset = self.energy[onset_i] 166 self._offset = self.energy[offset_i]
Set absorption onset and offset.
168 def eval_sp_absorb(self, energy: Sequence[Real, ...]) -> np.ndarray[float]: 169 r"""Evaluate spectral absorbance at given photon energy values. 170 171 Args: 172 energy: Photon energy values [$\mathrm{eV}$]. 173 174 Returns: 175 Spectral absorbance values. 176 """ 177 energy = np.asarray(energy, dtype=float).flatten() 178 sp_absorb = np.array([], dtype=float) 179 for e in energy: 180 if e < np.amin(self.energy) or e >= np.amax(self.energy): 181 sp_absorb = np.append(sp_absorb, np.nan) 182 else: 183 i = np.searchsorted(self.energy, e, side="right") 184 pcoeff = self.pcoeff[:, i - 1] 185 sp_absorb = np.append( 186 sp_absorb, 187 np.polynomial.polynomial.polyval(e, pcoeff), 188 ) 189 return sp_absorb
Evaluate spectral absorbance at given photon energy values.
Arguments:
- energy: Photon energy values [$\mathrm{eV}$].
Returns:
Spectral absorbance values.
191 def plot_sp_absorb_vs_energy( 192 self, 193 *, 194 energy_min: Real, 195 energy_max: Real, 196 energy_inc: Real, 197 ax: plt.Axes | None = None, 198 ) -> plt.Figure | None: 199 r"""Plot spectral absorbance vs photon energy. 200 201 Args: 202 energy_min: Photon energy minimum [$\mathrm{eV}$]. 203 energy_max: Photon energy maximum [$\mathrm{eV}$]. 204 energy_inc: Photon energy increment [$\mathrm{eV}$]. 205 ax: Plot axes. If None, new figure and axes are created. 206 207 Returns: 208 Plot figure. 209 """ 210 if not 0.0 <= energy_min < energy_max < np.inf: 211 print("Photon energy range is not valid!") 212 return None 213 214 if not 0.0 < energy_inc < energy_max - energy_min: 215 print("Photon energy increment is not valid!") 216 return None 217 218 energy = np.arange(energy_min, energy_max + energy_inc, energy_inc) 219 sp_absorb = self.eval_sp_absorb(energy) 220 221 if ax is None: 222 fig, ax = plt.subplots(tight_layout=True) 223 ax.set_xlabel(r"Photon energy / eV") 224 ax.set_ylabel(r"Spectral absorbance") 225 ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 226 else: 227 fig = ax.get_figure() 228 229 ax.plot(energy, sp_absorb, color="k") 230 ax.fill_between(energy, sp_absorb, alpha=0.5) 231 return fig
Plot spectral absorbance vs photon energy.
Arguments:
- energy_min: Photon energy minimum [$\mathrm{eV}$].
- energy_max: Photon energy maximum [$\mathrm{eV}$].
- energy_inc: Photon energy increment [$\mathrm{eV}$].
- ax: Plot axes. If None, new figure and axes are created.
Returns:
Plot figure.
233 @classmethod 234 def from_data( 235 cls, 236 energy: Sequence[Real, ...], 237 sp_absorb: Sequence[Real, ...], 238 ) -> Self: 239 r"""Create SpectralAbsorbancePPoly from data. 240 241 Args: 242 energy: Photon energy values [$\mathrm{eV}$]. 243 sp_absorb: Spectral absorbance values. 244 245 Returns: 246 Spectral absorbance piecewise polynomial. 247 248 Raises: 249 ValueError: If there are duplicate photon energy values. 250 """ 251 energy = np.asarray(energy, dtype=float).flatten() 252 sp_absorb = np.asarray(sp_absorb, dtype=float).flatten() 253 254 sort_i = np.argsort(energy) 255 energy = energy[sort_i] 256 sp_absorb = sp_absorb[sort_i] 257 sp_absorb[np.isclose(sp_absorb, 0.0)] = 0.0 258 259 if not np.array_equal(energy, np.unique(energy)): 260 raise ValueError("Duplicate photon energy values!") 261 262 slope = np.diff(sp_absorb) / np.diff(energy) 263 offset = sp_absorb[:-1] - slope * energy[:-1] 264 pcoeff = np.asarray([offset, slope]) 265 266 return cls(energy, pcoeff)
Create SpectralAbsorbancePPoly from data.
Arguments:
- energy: Photon energy values [$\mathrm{eV}$].
- sp_absorb: Spectral absorbance values.
Returns:
Spectral absorbance piecewise polynomial.
Raises:
- ValueError: If there are duplicate photon energy values.
269class ResolvedSpectralAbsorbancePPoly: 270 r"""Resolved spectral absorbance piecewise polynomials. 271 272 Args: 273 ii: Intermediate-intermediate spectral absorbance. 274 vi: Valence-intermediate spectral absorbance. 275 ic: Intermediate-conduction spectral absorbance. 276 vc: Valence-conduction spectral absorbance. 277 """ 278 279 def __init__( 280 self, 281 *, 282 ii: SpectralAbsorbancePPoly | Any = None, 283 vi: SpectralAbsorbancePPoly | Any = None, 284 ic: SpectralAbsorbancePPoly | Any = None, 285 vc: SpectralAbsorbancePPoly | Any = None, 286 ) -> None: 287 r"""Initialize ResolvedSpectralAbsorbancePPoly object.""" 288 self.set_rsv_sp_absorb_ppoly(ii=ii, vi=vi, ic=ic, vc=vc) 289 290 def __repr__(self) -> str: 291 r"""Represent ResolvedSpectralAbsorbancePPoly object.""" 292 blocks = [] 293 if self.ii is not None: 294 blocks.append(f"{self.ii}".replace(":", " (ii):")) 295 if self.vi is not None: 296 blocks.append(f"{self.vi}".replace(":", " (vi):")) 297 if self.ic is not None: 298 blocks.append(f"{self.ic}".replace(":", " (ic):")) 299 if self.vc is not None: 300 blocks.append(f"{self.vc}".replace(":", " (vc):")) 301 return "\n\n".join(blocks) 302 303 @property 304 def ii(self) -> SpectralAbsorbancePPoly | None: 305 r"""Intermediate-intermediate spectral absorbance.""" 306 return self._ii 307 308 @property 309 def vi(self) -> SpectralAbsorbancePPoly | None: 310 r"""Valence-intermediate spectral absorbance.""" 311 return self._vi 312 313 @property 314 def ic(self) -> SpectralAbsorbancePPoly | None: 315 r"""Intermediate-conduction spectral absorbance.""" 316 return self._ic 317 318 @property 319 def vc(self) -> SpectralAbsorbancePPoly | None: 320 r"""Valence-conduction spectral absorbance.""" 321 return self._vc 322 323 def clear_cached_properties(self) -> None: 324 r"""Clear cached properties.""" 325 326 def set_rsv_sp_absorb_ppoly( 327 self, 328 *, 329 ii: SpectralAbsorbancePPoly | Any = None, 330 vi: SpectralAbsorbancePPoly | Any = None, 331 ic: SpectralAbsorbancePPoly | Any = None, 332 vc: SpectralAbsorbancePPoly | Any = None, 333 ) -> None: 334 r"""Set resolved spectral absorbance piecewise polynomials. 335 336 Args: 337 ii: Intermediate-intermediate spectral absorbance. 338 vi: Valence-intermediate spectral absorbance. 339 ic: Intermediate-conduction spectral absorbance. 340 vc: Valence-conduction spectral absorbance. 341 342 Raises: 343 ValueError: If spectral absorbance piecewise polynomials do not 344 have the same photon energy partition or degree. 345 """ 346 energy_xy_seq = [] 347 pcoeff_xy_seq = [] 348 for xy in [ii, vi, ic, vc]: 349 if isinstance(xy, SpectralAbsorbancePPoly): 350 energy_xy_seq.append(xy.energy) 351 pcoeff_xy_seq.append(xy.pcoeff) 352 353 for energy_xy in energy_xy_seq: 354 if not np.array_equal(energy_xy, energy_xy_seq[0]): 355 raise ValueError( 356 "Spectral absorbance piecewise polynomials " 357 + "must have the same photon energy partition!" 358 ) 359 360 for pcoeff_xy in pcoeff_xy_seq: 361 if not pcoeff_xy.shape == pcoeff_xy_seq[0].shape: 362 raise ValueError( 363 "Spectral absorbance piecewise polynomials " 364 + "must have the same degree!" 365 ) 366 367 self._ii = ii if isinstance(ii, SpectralAbsorbancePPoly) else None 368 self._vi = vi if isinstance(vi, SpectralAbsorbancePPoly) else None 369 self._ic = ic if isinstance(ic, SpectralAbsorbancePPoly) else None 370 self._vc = vc if isinstance(vc, SpectralAbsorbancePPoly) else None 371 self.clear_cached_properties() 372 373 def plot_rsv_sp_absorb_vs_energy( 374 self, 375 *, 376 energy_min: Real, 377 energy_max: Real, 378 energy_inc: Real, 379 ax: plt.Axes | None = None, 380 ) -> plt.Figure | None: 381 r"""Plot resolved spectral absorbance vs photon energy. 382 383 Args: 384 energy_min: Photon energy minimum [$\mathrm{eV}$]. 385 energy_max: Photon energy maximum [$\mathrm{eV}$]. 386 energy_inc: Photon energy increment [$\mathrm{eV}$]. 387 ax: Plot axes. If None, new figure and axes are created. 388 389 Returns: 390 Plot figure. 391 """ 392 if not 0.0 <= energy_min < energy_max < np.inf: 393 print("Photon energy range is not valid!") 394 return None 395 396 if not 0.0 < energy_inc < energy_max - energy_min: 397 print("Photon energy increment is not valid!") 398 return None 399 400 energy = np.arange(energy_min, energy_max + energy_inc, energy_inc) 401 402 rsv_sp_absorb = {} 403 for xy in ["ii", "vi", "ic", "vc"]: 404 sp_absorb_xy_ppoly = getattr(self, xy) 405 if sp_absorb_xy_ppoly is not None: 406 sp_absorb_xy = sp_absorb_xy_ppoly.eval_sp_absorb(energy) 407 else: 408 sp_absorb_xy = 0.0 * energy 409 rsv_sp_absorb.update({xy: sp_absorb_xy}) 410 411 sp_absorb_tot = np.sum( 412 [sp_absorb_xy for sp_absorb_xy in rsv_sp_absorb.values()], 413 axis=0, 414 ) 415 416 if ax is None: 417 fig, ax = plt.subplots(tight_layout=True) 418 ax.set_xlabel(r"Photon energy / $\mathrm{eV}$") 419 ax.set_ylabel(r"Spectral absorbance") 420 ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 421 else: 422 fig = ax.get_figure() 423 424 ax.plot(energy, sp_absorb_tot, color="k") 425 ax.stackplot( 426 energy, 427 rsv_sp_absorb.values(), 428 labels=rsv_sp_absorb.keys(), 429 ) 430 ax.legend( 431 loc="lower center", 432 bbox_to_anchor=[0.5, 1.0], 433 frameon=False, 434 ncols=len(rsv_sp_absorb), 435 ) 436 return fig 437 438 @classmethod 439 def sq1961( 440 cls, 441 *, 442 bandgap_vc: Real, 443 bandwidth_vc: Real, 444 ) -> Self | None: 445 r"""Shockley-Queisser resolved spectral absorbance piecewise polynomials. 446 447 Note: 448 See [original article](https://dx.doi.org/10.1063%2F1.1736034) for 449 details. 450 451 Args: 452 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 453 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 454 455 Returns: 456 Resolved spectral absorbance piecewise polynomial. 457 """ 458 if not 0.0 < bandgap_vc < np.inf: 459 print("Valence-conduction optical band gap must be finite and > 0!") 460 return None 461 462 if not 0.0 <= bandwidth_vc <= np.inf: 463 print("Valence-conduction optical band width must be nonnegative!") 464 return None 465 466 onset = bandgap_vc 467 offset = bandgap_vc + bandwidth_vc 468 energy = np.unique([0.0, onset, offset, np.inf]).astype(float) 469 470 energy_mid = 0.5 * (energy[:-1] + energy[1:]) 471 sp_absorp = rectangle(energy_mid, a=onset, b=offset) 472 pcoeff = sp_absorp.reshape(1, -1) 473 474 sp_absorb_ppoly = SpectralAbsorbancePPoly(energy, pcoeff) 475 return cls(vc=sp_absorb_ppoly) 476 477 @classmethod 478 def lh2008( 479 cls, 480 *, 481 bandgap_vi: Real, 482 bandgap_ic: Real, 483 bandgap_vc: Real, 484 bandwidth_ii: Real, 485 bandwidth_vi: Real, 486 bandwidth_ic: Real, 487 bandwidth_vc: Real, 488 variant: str, 489 ) -> Self | None: 490 r"""Levi-Honsberg resolved spectral absorbance piecewise polynomials. 491 492 Note: 493 See 494 [original article](http://dx.doi.org/10.1103%2FPhysRevB.78.165122) 495 for details. 496 497 Args: 498 bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$]. 499 bandgap_ic: Intermediate-conduction optical band gap 500 [$\mathrm{eV}$]. 501 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 502 bandwidth_ii: Intermediate-intermediate optical band width 503 [$\mathrm{eV}$]. 504 bandwidth_vi: Valence-intermediate optical band width 505 [$\mathrm{eV}$]. 506 bandwidth_ic: Intermediate-conduction optical band width 507 [$\mathrm{eV}$]. 508 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 509 variant: Model variant ('equal', 'inter', or 'intra'). 510 511 Returns: 512 Resolved spectral absorbance piecewise polynomials. 513 """ 514 if not ( 515 0.0 < bandgap_vi < np.inf 516 and 0.0 < bandgap_ic < np.inf 517 and 0.0 < bandgap_vc < np.inf 518 ): 519 print("Optical band gaps must be finite and positive!") 520 return None 521 522 if not ( 523 bandwidth_ii >= 0.0 524 and bandwidth_vi >= 0.0 525 and bandwidth_ic >= 0.0 526 and bandwidth_vc >= 0.0 527 ): 528 print("Optical band widths must be nonnegative!") 529 return None 530 531 if not bandwidth_ii < np.inf: 532 print("Intermediate band width must be finite!") 533 return None 534 535 if not variant in {"equal", "inter", "intra"}: 536 print("Variant must be 'equal', 'inter' or 'intra'!") 537 return None 538 539 if not bandgap_vi + bandgap_ic + bandwidth_ii <= bandgap_vc: 540 if np.isclose(bandgap_vi + bandgap_ic + bandwidth_ii, bandgap_vc): 541 # print("Removing floating-point representation error...") 542 bandgap_vc = bandgap_vi + bandgap_ic + bandwidth_ii 543 else: 544 print("Valence-conduction optical bandgap is too small!") 545 return None 546 547 if variant == "inter": 548 onset = [0.0] 549 offset = [min(bandwidth_ii, bandgap_vi, bandgap_ic)] 550 else: 551 onset = [0.0] 552 offset = [bandwidth_ii] 553 554 if variant == "intra": 555 onset += [ 556 max(bandgap_vi, bandwidth_ii), 557 max(bandgap_ic, bandwidth_ii), 558 max(bandgap_vc, bandwidth_ii), 559 ] 560 offset += [ 561 max(bandgap_vi + bandwidth_vi, bandwidth_ii), 562 max(bandgap_ic + bandwidth_ic, bandwidth_ii), 563 max(bandgap_vc + bandwidth_vc, bandwidth_ii), 564 ] 565 else: 566 onset += [bandgap_vi, bandgap_ic, bandgap_vc] 567 offset += [ 568 bandgap_vi + bandwidth_vi, 569 bandgap_ic + bandwidth_ic, 570 bandgap_vc + bandwidth_vc, 571 ] 572 573 energy = np.unique([0.0, np.inf] + onset + offset).astype(float) 574 575 energy_mid = 0.5 * (energy[:-1] + energy[1:]) 576 sp_absorp = rectangle(energy_mid, a=onset, b=offset) 577 sp_absorp_tot = np.sum(sp_absorp, axis=0) 578 pcoeff = sp_absorp / np.where(sp_absorp_tot == 0.0, 1.0, sp_absorp_tot) 579 580 rsv_sp_absorb_ppoly = { 581 xy: SpectralAbsorbancePPoly(energy, pcoeff_xy.reshape(1, -1)) 582 for xy, pcoeff_xy in zip(["ii", "vi", "ic", "vc"], pcoeff) 583 } 584 return cls(**rsv_sp_absorb_ppoly) 585 586 @classmethod 587 def from_data( 588 cls, 589 energy: Sequence[Real, ...], 590 rsv_sp_absorb: Mapping[str, Sequence[Real, ...]], 591 ) -> Self: 592 r"""Create resolved spectral absorbance piecewise polynomials from data. 593 594 Args: 595 energy: Photon energy values [$\mathrm{eV}$]. 596 rsv_sp_absorb: Mapping of electronic transition labels ('ii', 'vi', 597 'ic', 'vc') into corresponding spectral absorbance values. 598 599 Returns: 600 Resolved spectral absorbace piecewise polynomials. 601 """ 602 rsv_sp_absorb = { 603 xy: SpectralAbsorbancePPoly.from_data(energy, sp_absorb_xy) 604 for xy, sp_absorb_xy in rsv_sp_absorb.items() 605 if sp_absorb_xy is not None 606 } 607 return cls(**rsv_sp_absorb)
Resolved spectral absorbance piecewise polynomials.
Arguments:
- ii: Intermediate-intermediate spectral absorbance.
- vi: Valence-intermediate spectral absorbance.
- ic: Intermediate-conduction spectral absorbance.
- vc: Valence-conduction spectral absorbance.
279 def __init__( 280 self, 281 *, 282 ii: SpectralAbsorbancePPoly | Any = None, 283 vi: SpectralAbsorbancePPoly | Any = None, 284 ic: SpectralAbsorbancePPoly | Any = None, 285 vc: SpectralAbsorbancePPoly | Any = None, 286 ) -> None: 287 r"""Initialize ResolvedSpectralAbsorbancePPoly object.""" 288 self.set_rsv_sp_absorb_ppoly(ii=ii, vi=vi, ic=ic, vc=vc)
Initialize ResolvedSpectralAbsorbancePPoly object.
303 @property 304 def ii(self) -> SpectralAbsorbancePPoly | None: 305 r"""Intermediate-intermediate spectral absorbance.""" 306 return self._ii
Intermediate-intermediate spectral absorbance.
308 @property 309 def vi(self) -> SpectralAbsorbancePPoly | None: 310 r"""Valence-intermediate spectral absorbance.""" 311 return self._vi
Valence-intermediate spectral absorbance.
313 @property 314 def ic(self) -> SpectralAbsorbancePPoly | None: 315 r"""Intermediate-conduction spectral absorbance.""" 316 return self._ic
Intermediate-conduction spectral absorbance.
318 @property 319 def vc(self) -> SpectralAbsorbancePPoly | None: 320 r"""Valence-conduction spectral absorbance.""" 321 return self._vc
Valence-conduction spectral absorbance.
326 def set_rsv_sp_absorb_ppoly( 327 self, 328 *, 329 ii: SpectralAbsorbancePPoly | Any = None, 330 vi: SpectralAbsorbancePPoly | Any = None, 331 ic: SpectralAbsorbancePPoly | Any = None, 332 vc: SpectralAbsorbancePPoly | Any = None, 333 ) -> None: 334 r"""Set resolved spectral absorbance piecewise polynomials. 335 336 Args: 337 ii: Intermediate-intermediate spectral absorbance. 338 vi: Valence-intermediate spectral absorbance. 339 ic: Intermediate-conduction spectral absorbance. 340 vc: Valence-conduction spectral absorbance. 341 342 Raises: 343 ValueError: If spectral absorbance piecewise polynomials do not 344 have the same photon energy partition or degree. 345 """ 346 energy_xy_seq = [] 347 pcoeff_xy_seq = [] 348 for xy in [ii, vi, ic, vc]: 349 if isinstance(xy, SpectralAbsorbancePPoly): 350 energy_xy_seq.append(xy.energy) 351 pcoeff_xy_seq.append(xy.pcoeff) 352 353 for energy_xy in energy_xy_seq: 354 if not np.array_equal(energy_xy, energy_xy_seq[0]): 355 raise ValueError( 356 "Spectral absorbance piecewise polynomials " 357 + "must have the same photon energy partition!" 358 ) 359 360 for pcoeff_xy in pcoeff_xy_seq: 361 if not pcoeff_xy.shape == pcoeff_xy_seq[0].shape: 362 raise ValueError( 363 "Spectral absorbance piecewise polynomials " 364 + "must have the same degree!" 365 ) 366 367 self._ii = ii if isinstance(ii, SpectralAbsorbancePPoly) else None 368 self._vi = vi if isinstance(vi, SpectralAbsorbancePPoly) else None 369 self._ic = ic if isinstance(ic, SpectralAbsorbancePPoly) else None 370 self._vc = vc if isinstance(vc, SpectralAbsorbancePPoly) else None 371 self.clear_cached_properties()
Set resolved spectral absorbance piecewise polynomials.
Arguments:
- ii: Intermediate-intermediate spectral absorbance.
- vi: Valence-intermediate spectral absorbance.
- ic: Intermediate-conduction spectral absorbance.
- vc: Valence-conduction spectral absorbance.
Raises:
- ValueError: If spectral absorbance piecewise polynomials do not have the same photon energy partition or degree.
373 def plot_rsv_sp_absorb_vs_energy( 374 self, 375 *, 376 energy_min: Real, 377 energy_max: Real, 378 energy_inc: Real, 379 ax: plt.Axes | None = None, 380 ) -> plt.Figure | None: 381 r"""Plot resolved spectral absorbance vs photon energy. 382 383 Args: 384 energy_min: Photon energy minimum [$\mathrm{eV}$]. 385 energy_max: Photon energy maximum [$\mathrm{eV}$]. 386 energy_inc: Photon energy increment [$\mathrm{eV}$]. 387 ax: Plot axes. If None, new figure and axes are created. 388 389 Returns: 390 Plot figure. 391 """ 392 if not 0.0 <= energy_min < energy_max < np.inf: 393 print("Photon energy range is not valid!") 394 return None 395 396 if not 0.0 < energy_inc < energy_max - energy_min: 397 print("Photon energy increment is not valid!") 398 return None 399 400 energy = np.arange(energy_min, energy_max + energy_inc, energy_inc) 401 402 rsv_sp_absorb = {} 403 for xy in ["ii", "vi", "ic", "vc"]: 404 sp_absorb_xy_ppoly = getattr(self, xy) 405 if sp_absorb_xy_ppoly is not None: 406 sp_absorb_xy = sp_absorb_xy_ppoly.eval_sp_absorb(energy) 407 else: 408 sp_absorb_xy = 0.0 * energy 409 rsv_sp_absorb.update({xy: sp_absorb_xy}) 410 411 sp_absorb_tot = np.sum( 412 [sp_absorb_xy for sp_absorb_xy in rsv_sp_absorb.values()], 413 axis=0, 414 ) 415 416 if ax is None: 417 fig, ax = plt.subplots(tight_layout=True) 418 ax.set_xlabel(r"Photon energy / $\mathrm{eV}$") 419 ax.set_ylabel(r"Spectral absorbance") 420 ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) 421 else: 422 fig = ax.get_figure() 423 424 ax.plot(energy, sp_absorb_tot, color="k") 425 ax.stackplot( 426 energy, 427 rsv_sp_absorb.values(), 428 labels=rsv_sp_absorb.keys(), 429 ) 430 ax.legend( 431 loc="lower center", 432 bbox_to_anchor=[0.5, 1.0], 433 frameon=False, 434 ncols=len(rsv_sp_absorb), 435 ) 436 return fig
Plot resolved spectral absorbance vs photon energy.
Arguments:
- energy_min: Photon energy minimum [$\mathrm{eV}$].
- energy_max: Photon energy maximum [$\mathrm{eV}$].
- energy_inc: Photon energy increment [$\mathrm{eV}$].
- ax: Plot axes. If None, new figure and axes are created.
Returns:
Plot figure.
438 @classmethod 439 def sq1961( 440 cls, 441 *, 442 bandgap_vc: Real, 443 bandwidth_vc: Real, 444 ) -> Self | None: 445 r"""Shockley-Queisser resolved spectral absorbance piecewise polynomials. 446 447 Note: 448 See [original article](https://dx.doi.org/10.1063%2F1.1736034) for 449 details. 450 451 Args: 452 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 453 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 454 455 Returns: 456 Resolved spectral absorbance piecewise polynomial. 457 """ 458 if not 0.0 < bandgap_vc < np.inf: 459 print("Valence-conduction optical band gap must be finite and > 0!") 460 return None 461 462 if not 0.0 <= bandwidth_vc <= np.inf: 463 print("Valence-conduction optical band width must be nonnegative!") 464 return None 465 466 onset = bandgap_vc 467 offset = bandgap_vc + bandwidth_vc 468 energy = np.unique([0.0, onset, offset, np.inf]).astype(float) 469 470 energy_mid = 0.5 * (energy[:-1] + energy[1:]) 471 sp_absorp = rectangle(energy_mid, a=onset, b=offset) 472 pcoeff = sp_absorp.reshape(1, -1) 473 474 sp_absorb_ppoly = SpectralAbsorbancePPoly(energy, pcoeff) 475 return cls(vc=sp_absorb_ppoly)
Shockley-Queisser resolved spectral absorbance piecewise polynomials.
Note:
See original article for details.
Arguments:
- bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
- bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
Returns:
Resolved spectral absorbance piecewise polynomial.
477 @classmethod 478 def lh2008( 479 cls, 480 *, 481 bandgap_vi: Real, 482 bandgap_ic: Real, 483 bandgap_vc: Real, 484 bandwidth_ii: Real, 485 bandwidth_vi: Real, 486 bandwidth_ic: Real, 487 bandwidth_vc: Real, 488 variant: str, 489 ) -> Self | None: 490 r"""Levi-Honsberg resolved spectral absorbance piecewise polynomials. 491 492 Note: 493 See 494 [original article](http://dx.doi.org/10.1103%2FPhysRevB.78.165122) 495 for details. 496 497 Args: 498 bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$]. 499 bandgap_ic: Intermediate-conduction optical band gap 500 [$\mathrm{eV}$]. 501 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 502 bandwidth_ii: Intermediate-intermediate optical band width 503 [$\mathrm{eV}$]. 504 bandwidth_vi: Valence-intermediate optical band width 505 [$\mathrm{eV}$]. 506 bandwidth_ic: Intermediate-conduction optical band width 507 [$\mathrm{eV}$]. 508 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 509 variant: Model variant ('equal', 'inter', or 'intra'). 510 511 Returns: 512 Resolved spectral absorbance piecewise polynomials. 513 """ 514 if not ( 515 0.0 < bandgap_vi < np.inf 516 and 0.0 < bandgap_ic < np.inf 517 and 0.0 < bandgap_vc < np.inf 518 ): 519 print("Optical band gaps must be finite and positive!") 520 return None 521 522 if not ( 523 bandwidth_ii >= 0.0 524 and bandwidth_vi >= 0.0 525 and bandwidth_ic >= 0.0 526 and bandwidth_vc >= 0.0 527 ): 528 print("Optical band widths must be nonnegative!") 529 return None 530 531 if not bandwidth_ii < np.inf: 532 print("Intermediate band width must be finite!") 533 return None 534 535 if not variant in {"equal", "inter", "intra"}: 536 print("Variant must be 'equal', 'inter' or 'intra'!") 537 return None 538 539 if not bandgap_vi + bandgap_ic + bandwidth_ii <= bandgap_vc: 540 if np.isclose(bandgap_vi + bandgap_ic + bandwidth_ii, bandgap_vc): 541 # print("Removing floating-point representation error...") 542 bandgap_vc = bandgap_vi + bandgap_ic + bandwidth_ii 543 else: 544 print("Valence-conduction optical bandgap is too small!") 545 return None 546 547 if variant == "inter": 548 onset = [0.0] 549 offset = [min(bandwidth_ii, bandgap_vi, bandgap_ic)] 550 else: 551 onset = [0.0] 552 offset = [bandwidth_ii] 553 554 if variant == "intra": 555 onset += [ 556 max(bandgap_vi, bandwidth_ii), 557 max(bandgap_ic, bandwidth_ii), 558 max(bandgap_vc, bandwidth_ii), 559 ] 560 offset += [ 561 max(bandgap_vi + bandwidth_vi, bandwidth_ii), 562 max(bandgap_ic + bandwidth_ic, bandwidth_ii), 563 max(bandgap_vc + bandwidth_vc, bandwidth_ii), 564 ] 565 else: 566 onset += [bandgap_vi, bandgap_ic, bandgap_vc] 567 offset += [ 568 bandgap_vi + bandwidth_vi, 569 bandgap_ic + bandwidth_ic, 570 bandgap_vc + bandwidth_vc, 571 ] 572 573 energy = np.unique([0.0, np.inf] + onset + offset).astype(float) 574 575 energy_mid = 0.5 * (energy[:-1] + energy[1:]) 576 sp_absorp = rectangle(energy_mid, a=onset, b=offset) 577 sp_absorp_tot = np.sum(sp_absorp, axis=0) 578 pcoeff = sp_absorp / np.where(sp_absorp_tot == 0.0, 1.0, sp_absorp_tot) 579 580 rsv_sp_absorb_ppoly = { 581 xy: SpectralAbsorbancePPoly(energy, pcoeff_xy.reshape(1, -1)) 582 for xy, pcoeff_xy in zip(["ii", "vi", "ic", "vc"], pcoeff) 583 } 584 return cls(**rsv_sp_absorb_ppoly)
Levi-Honsberg resolved spectral absorbance piecewise polynomials.
Note:
See original article for details.
Arguments:
- bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$].
- bandgap_ic: Intermediate-conduction optical band gap [$\mathrm{eV}$].
- bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
- bandwidth_ii: Intermediate-intermediate optical band width [$\mathrm{eV}$].
- bandwidth_vi: Valence-intermediate optical band width [$\mathrm{eV}$].
- bandwidth_ic: Intermediate-conduction optical band width [$\mathrm{eV}$].
- bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
- variant: Model variant ('equal', 'inter', or 'intra').
Returns:
Resolved spectral absorbance piecewise polynomials.
586 @classmethod 587 def from_data( 588 cls, 589 energy: Sequence[Real, ...], 590 rsv_sp_absorb: Mapping[str, Sequence[Real, ...]], 591 ) -> Self: 592 r"""Create resolved spectral absorbance piecewise polynomials from data. 593 594 Args: 595 energy: Photon energy values [$\mathrm{eV}$]. 596 rsv_sp_absorb: Mapping of electronic transition labels ('ii', 'vi', 597 'ic', 'vc') into corresponding spectral absorbance values. 598 599 Returns: 600 Resolved spectral absorbace piecewise polynomials. 601 """ 602 rsv_sp_absorb = { 603 xy: SpectralAbsorbancePPoly.from_data(energy, sp_absorb_xy) 604 for xy, sp_absorb_xy in rsv_sp_absorb.items() 605 if sp_absorb_xy is not None 606 } 607 return cls(**rsv_sp_absorb)
Create resolved spectral absorbance piecewise polynomials from data.
Arguments:
- energy: Photon energy values [$\mathrm{eV}$].
- rsv_sp_absorb: Mapping of electronic transition labels ('ii', 'vi', 'ic', 'vc') into corresponding spectral absorbance values.
Returns:
Resolved spectral absorbace piecewise polynomials.