scambio.balance
Solar cell detailed balance model.
1r"""Solar cell detailed balance model.""" 2 3import numpy as np 4from collections.abc import Mapping, Sequence 5from numbers import Real 6from scipy.optimize import root_scalar 7from typing import Self 8from scambio.absorbance import ( 9 ResolvedSpectralAbsorbancePPoly, 10 SpectralAbsorbancePPoly, 11) 12from scambio.constant import Constant 13from scambio.special import bose_einstein_integr 14 15 16class RadiativeExchangeChannel: 17 r"""Radiative exchange channel. 18 19 Args: 20 sp_absorb_ppoly: Spectral absorbance piecewise polynomial. 21 temperature: Temperature [$\mathrm{K}$]. 22 potential: Chemical potential [$\mathrm{eV}$]. 23 concentration: Sunlight concentration factor. 24 25 Raises: 26 TypeError: If sp_absorb_ppoly is not a SpectralAbsorbancePPoly object. 27 ValueError: If temperature, chemical potential, or sunlight 28 concentration factor are unphysical. 29 """ 30 31 def __init__( 32 self, 33 sp_absorb_ppoly: SpectralAbsorbancePPoly, 34 *, 35 temperature: Real, 36 potential: Real, 37 concentration: Real, 38 ) -> None: 39 r"""Initialize RadiativeExchangeChannel object.""" 40 self.sp_absorb_ppoly = sp_absorb_ppoly 41 self.temperature = temperature 42 self.potential = potential 43 self.concentration = concentration 44 45 @property 46 def sp_absorb_ppoly(self) -> SpectralAbsorbancePPoly: 47 r"""Spectral absorbance piecewise polynomial.""" 48 return self._sp_absorb_ppoly 49 50 @sp_absorb_ppoly.setter 51 def sp_absorb_ppoly(self, arg: SpectralAbsorbancePPoly) -> None: 52 if not isinstance(arg, SpectralAbsorbancePPoly): 53 raise TypeError("Not a SpectralAbsorbancePPoly object!") 54 self._sp_absorb_ppoly = arg 55 self.clear_cached_properties() 56 57 @property 58 def temperature(self) -> float: 59 r"""Temperature [$\mathrm{K}$].""" 60 return self._temperature 61 62 @temperature.setter 63 def temperature(self, arg: Real) -> None: 64 if not isinstance(arg, Real) or not arg > 0.0: 65 raise ValueError("Temperature must be a positive number!") 66 self._temperature = float(arg) 67 self.clear_cached_properties() 68 69 @property 70 def potential(self) -> float: 71 r"""Chemical potential [$\mathrm{eV}$].""" 72 return self._potential 73 74 @potential.setter 75 def potential(self, arg: Real) -> None: 76 if not isinstance(arg, Real): 77 raise ValueError("Chemical potential must be a real number!") 78 self._potential = float(arg) 79 self.clear_cached_properties() 80 81 @property 82 def concentration(self) -> float: 83 r"""Sunlight concentration factor.""" 84 return self._concentration 85 86 @concentration.setter 87 def concentration(self, arg: Real) -> None: 88 if not isinstance(arg, Real) or not (0.0 <= arg <= 46200.0): 89 raise ValueError( 90 "Sunlight concentration factor must be between 0 and 46200!" 91 ) 92 self._concentration = float(arg) 93 self.clear_cached_properties() 94 95 @property 96 def phot_flux(self) -> float: 97 r"""Photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 98 if self._phot_flux is None: 99 self._phot_flux = 0.0 100 if not np.isnan(self.sp_absorb_ppoly.onset): 101 for i, pcoeff_i in enumerate(self.sp_absorb_ppoly.pcoeff.T): 102 energy_lb = self.sp_absorb_ppoly.energy[i] 103 energy_ub = self.sp_absorb_ppoly.energy[i + 1] 104 for j, pcoeff_ij in enumerate(pcoeff_i): 105 if pcoeff_ij != 0.0: 106 order = 2 + j 107 self._phot_flux += pcoeff_ij * ( 108 bose_einstein_integr( 109 order, 110 energy_lb, 111 temperature=self.temperature, 112 potential=self.potential, 113 ) 114 - bose_einstein_integr( 115 order, 116 energy_ub, 117 temperature=self.temperature, 118 potential=self.potential, 119 ) 120 ) 121 if self.concentration != 0.0: 122 self._phot_flux -= ( 123 pcoeff_ij 124 * self.concentration 125 * Constant.fsun 126 * ( 127 bose_einstein_integr( 128 order, 129 energy_lb, 130 temperature=Constant.tsun, 131 potential=Constant.usun, 132 ) 133 - bose_einstein_integr( 134 order, 135 energy_ub, 136 temperature=Constant.tsun, 137 potential=Constant.usun, 138 ) 139 ) 140 ) 141 return self._phot_flux 142 143 def clear_cached_properties(self) -> None: 144 r"""Clear cached properties.""" 145 self._phot_flux = None 146 147 148class DetailedBalanceModel: 149 r"""Solar cell detailed balance model. 150 151 Note: 152 Applicable to Shockley-Queisser and intermediate band solar cells. 153 154 Args: 155 rsv_sp_absorb_ppoly: Resolved spectral absorbance piecewise polynomials. 156 temperature: Temperature [$\mathrm{K}$]. 157 voltage: Voltage [$\mathrm{V}$]. 158 concentration: Sunlight concentration factor. 159 160 Raises: 161 TypeError: If rsv_sp_absorb_ppoly is not a 162 ResolvedSpectralAbsorbancePPoly object. 163 ValueError: If temperature, voltage, or sunlight concentration factor 164 are unphysical. 165 """ 166 167 def __init__( 168 self, 169 rsv_sp_absorb_ppoly: ResolvedSpectralAbsorbancePPoly, 170 *, 171 temperature: Real, 172 voltage: Real, 173 concentration: Real, 174 ) -> None: 175 r"""Initialize RadiativeExchangeSystem object.""" 176 self.rsv_sp_absorb_ppoly = rsv_sp_absorb_ppoly 177 self.temperature = temperature 178 self.voltage = voltage 179 self.concentration = concentration 180 181 @property 182 def rsv_sp_absorb_ppoly(self) -> ResolvedSpectralAbsorbancePPoly: 183 r"""Resolved spectral absorbance piecewise polynomials.""" 184 return self._rsv_sp_absorb_ppoly 185 186 @rsv_sp_absorb_ppoly.setter 187 def rsv_sp_absorb_ppoly(self, arg: ResolvedSpectralAbsorbancePPoly) -> None: 188 if not isinstance(arg, ResolvedSpectralAbsorbancePPoly): 189 raise TypeError("Not a ResolvedSpectralAbsorbancePPoly object!") 190 self._rsv_sp_absorb_ppoly = arg 191 192 @property 193 def temperature(self) -> float: 194 r"""Temperature [$\mathrm{K}$].""" 195 return self._temperature 196 197 @temperature.setter 198 def temperature(self, arg: Real) -> None: 199 if not isinstance(arg, Real) or not arg > 0.0: 200 raise ValueError("Temperature must be a positive number!") 201 self._temperature = float(arg) 202 self.clear_cached_properties() 203 204 @property 205 def voltage(self) -> float: 206 r"""Voltage [$\mathrm{V}$].""" 207 return self._voltage 208 209 @voltage.setter 210 def voltage(self, arg: Real) -> None: 211 if not isinstance(arg, Real): 212 raise ValueError("Voltage must be a real number!") 213 self._voltage = float(arg) 214 self.clear_cached_properties() 215 216 @property 217 def concentration(self) -> float: 218 r"""Sunlight concentration factor.""" 219 return self._concentration 220 221 @concentration.setter 222 def concentration(self, arg: Real) -> None: 223 if not isinstance(arg, Real) or not (0.0 <= arg <= 46200.0): 224 raise ValueError( 225 "Sunlight concentration factor must be between 0 and 46200!" 226 ) 227 self._concentration = float(arg) 228 self.clear_cached_properties() 229 230 @property 231 def voltage_ub(self) -> float: 232 r"""Voltage upper bound [$\mathrm{V}$].""" 233 if self._voltage_ub is None: 234 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 235 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 236 if ( 237 sp_absorb_vi is not None 238 and not np.isnan(sp_absorb_vi.onset) 239 and sp_absorb_ic is not None 240 and not np.isnan(sp_absorb_ic.onset) 241 ): 242 voltage_ub = sp_absorb_vi.onset + sp_absorb_ic.onset 243 else: 244 sp_absorb_vc = self.rsv_sp_absorb_ppoly.vc 245 if sp_absorb_vc is not None and not np.isnan( 246 sp_absorb_vc.onset 247 ): 248 voltage_ub = sp_absorb_vc.onset 249 else: 250 voltage_ub = np.inf 251 self._voltage_ub = float(voltage_ub) 252 return self._voltage_ub 253 254 @property 255 def potential_vi_lb(self) -> float: 256 r"""Valence-intermediate chemical potential lower bound [$\mathrm{eV}$].""" 257 if self._potential_vi_lb is None: 258 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 259 if self.voltage < self.voltage_ub and sp_absorb_ic is not None: 260 onset_ic = sp_absorb_ic.onset 261 self._potential_vi_lb = self.voltage - onset_ic 262 else: 263 self._potential_vi_lb = np.nan 264 return self._potential_vi_lb 265 266 @property 267 def potential_vi_ub(self) -> float: 268 r"""Valence-intermediate chemical potential upper bound [$\mathrm{eV}$].""" 269 if self._potential_vi_ub is None: 270 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 271 if sp_absorb_vi is not None: 272 onset_vi = sp_absorb_vi.onset 273 self._potential_vi_ub = onset_vi 274 else: 275 self._potential_vi_ub = np.nan 276 return self._potential_vi_ub 277 278 @property 279 def potential_ii(self) -> float: 280 r"""Intermediate-intermediate chemical potential [$\mathrm{eV}$].""" 281 if self._potential_ii is None: 282 if self.rsv_sp_absorb_ppoly.ii is None: 283 self._potential_ii = np.nan 284 else: 285 self._potential_ii = 0.0 286 return self._potential_ii 287 288 @property 289 def potential_vi(self) -> float: 290 r"""Valence-intermediate chemical potential [$\mathrm{eV}$].""" 291 if self._potential_vi is None: 292 if self.voltage >= self.voltage_ub: 293 self._potential_vi = np.nan 294 elif np.isnan(self.potential_vi_lb) and np.isnan( 295 self.potential_vi_ub 296 ): 297 self._potential_vi = np.nan 298 else: 299 if np.isnan(self.potential_vi_lb): 300 potential_vi_b = self.potential_vi_ub 301 potential_vi_a = potential_vi_b - 0.1 302 curr_den_ib_a = self.curr_den_ib_vs_potential_vi( 303 potential_vi_a 304 ) 305 while curr_den_ib_a <= 0.0: 306 potential_vi_a -= 0.1 307 curr_den_ib_a = self.curr_den_ib_vs_potential_vi( 308 potential_vi_a 309 ) 310 elif np.isnan(self.potential_vi_ub): 311 potential_vi_a = self.potential_vi_lb 312 potential_vi_b = potential_vi_a + 0.1 313 curr_den_ib_b = self.curr_den_ib_vs_potential_vi( 314 potential_vi_b 315 ) 316 while curr_den_ib_b >= 0.0: 317 potential_vi_b += 0.1 318 curr_den_ib_b = self.curr_den_ib_vs_potential_vi( 319 potential_vi_b 320 ) 321 else: 322 potential_vi_a = self.potential_vi_lb 323 potential_vi_b = self.potential_vi_ub 324 325 def objective(potential_vi): 326 if potential_vi == potential_vi_a: 327 return np.inf 328 elif potential_vi == potential_vi_b: 329 return -np.inf 330 else: 331 return self.curr_den_ib_vs_potential_vi(potential_vi) 332 333 sol = root_scalar( 334 objective, 335 method="brentq", 336 bracket=[potential_vi_a, potential_vi_b], 337 ) 338 if not sol.converged: 339 print( 340 "Intermediate-valence chemical potential " 341 + "did not converge!" 342 ) 343 self._potential_vi = np.nan 344 else: 345 self._potential_vi = sol.root 346 347 return self._potential_vi 348 349 @property 350 def potential_ic(self) -> float: 351 r"""Intermediate-conduction chemical potential [$\mathrm{eV}$].""" 352 if self._potential_ic is None: 353 self._potential_ic = self.potential_vc - self.potential_vi 354 return self._potential_ic 355 356 @property 357 def potential_vc(self) -> float: 358 r"""Valence-conduction chemical potential [$\mathrm{eV}$].""" 359 if self._potential_vc is None: 360 if self.voltage >= self.voltage_ub: 361 self._potential_vc = np.nan 362 else: 363 self._potential_vc = self.voltage 364 return self._potential_vc 365 366 @property 367 def phot_flux_ii(self) -> float: 368 r"""Intermediate-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 369 if self._phot_flux_ii is None: 370 if self.rsv_sp_absorb_ppoly.ii is None: 371 self._phot_flux_ii = 0.0 372 else: 373 self._phot_flux_ii = RadiativeExchangeChannel( 374 self.rsv_sp_absorb_ppoly.ii, 375 temperature=self.temperature, 376 potential=self.potential_ii, 377 concentration=self.concentration, 378 ).phot_flux 379 return self._phot_flux_ii 380 381 @property 382 def phot_flux_vi(self) -> float: 383 r"""Valence-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 384 if self._phot_flux_vi is None: 385 self._phot_flux_vi = self.phot_flux_vi_vs_potential_vi( 386 self.potential_vi 387 ) 388 return self._phot_flux_vi 389 390 @property 391 def phot_flux_ic(self) -> float: 392 r"""Intermediate-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 393 if self._phot_flux_ic is None: 394 self._phot_flux_ic = self.phot_flux_ic_vs_potential_vi( 395 self.potential_vi 396 ) 397 return self._phot_flux_ic 398 399 @property 400 def phot_flux_vc(self) -> float: 401 r"""Valence-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 402 if self._phot_flux_vc is None: 403 if self.rsv_sp_absorb_ppoly.vc is None: 404 self._phot_flux_vc = 0.0 405 else: 406 self._phot_flux_vc = RadiativeExchangeChannel( 407 self.rsv_sp_absorb_ppoly.vc, 408 temperature=self.temperature, 409 potential=self.potential_vc, 410 concentration=self.concentration, 411 ).phot_flux 412 return self._phot_flux_vc 413 414 @property 415 def curr_den_ib(self) -> float: 416 r"""Intermediate-band current density [$\mathrm{mA \, cm^{-2}}$].""" 417 if self._curr_den_ib is None: 418 self._curr_den_ib = Constant.q * ( 419 self.phot_flux_ic - self.phot_flux_vi 420 ) 421 return self._curr_den_ib 422 423 @property 424 def curr_den_vb(self) -> float: 425 r"""Valence band current density [$\mathrm{mA \, cm^{-2}}$].""" 426 if self._curr_den_vb is None: 427 self._curr_den_vb = Constant.q * ( 428 self.phot_flux_vc + self.phot_flux_vi 429 ) 430 return self._curr_den_vb 431 432 @property 433 def curr_den_cb(self) -> float: 434 r"""Conduction band current density [$\mathrm{mA \, cm^{-2}}$].""" 435 if self._curr_den_cb is None: 436 self._curr_den_cb = Constant.q * ( 437 self.phot_flux_vc + self.phot_flux_ic 438 ) 439 return self._curr_den_cb 440 441 def clear_cached_properties(self) -> None: 442 r"""Clear cached properties.""" 443 self._voltage_ub = None 444 self._potential_vi_lb = None 445 self._potential_vi_ub = None 446 self._potential_ii = None 447 self._potential_vi = None 448 self._potential_ic = None 449 self._potential_vc = None 450 self._phot_flux_ii = None 451 self._phot_flux_vi = None 452 self._phot_flux_ic = None 453 self._phot_flux_vc = None 454 self._curr_den_ib = None 455 self._curr_den_vb = None 456 self._curr_den_cb = None 457 458 def phot_flux_vi_vs_potential_vi(self, potential_vi: Real) -> float: 459 r"""Valence-intermediate photon flux density vs chemical potential. 460 461 Args: 462 potential_vi: Valence-intermediate chemical potential 463 [$\mathrm{eV}$]. 464 465 Returns: 466 Valence-intermediate photon flux density 467 [$\mathrm{s^{-1} \, cm^{-2}}$]. 468 """ 469 if self.rsv_sp_absorb_ppoly.vi is None: 470 return 0.0 471 else: 472 return RadiativeExchangeChannel( 473 self.rsv_sp_absorb_ppoly.vi, 474 temperature=self.temperature, 475 potential=potential_vi, 476 concentration=self.concentration, 477 ).phot_flux 478 479 def phot_flux_ic_vs_potential_vi(self, potential_vi: Real) -> float: 480 r"""Intermediate-conduction photon flux density vs valence-intermediate chemical potential. 481 482 Args: 483 potential_vi: Valence-intermediate chemical potential [$\mathrm{eV}$]. 484 485 Returns: 486 Intermediate-conduction photon flux density 487 [$\mathrm{s^{-1} \, cm^{-2}}$]. 488 """ 489 if self.rsv_sp_absorb_ppoly.ic is None: 490 return 0.0 491 else: 492 return RadiativeExchangeChannel( 493 self.rsv_sp_absorb_ppoly.ic, 494 temperature=self.temperature, 495 potential=self.potential_vc - potential_vi, 496 concentration=self.concentration, 497 ).phot_flux 498 499 def curr_den_ib_vs_potential_vi(self, potential_vi: Real): 500 r"""Intermediate band current density vs vi chemical potential. 501 502 Args: 503 potential_vi: Valence-intermediate chemical potential 504 [$\mathrm{eV}$]. 505 506 Returns: 507 Intermediate band current density [$\mathrm{mA \, cm^{-2}}$]. 508 """ 509 phot_flux_vi = self.phot_flux_vi_vs_potential_vi(potential_vi) 510 phot_flux_ic = self.phot_flux_ic_vs_potential_vi(potential_vi) 511 return Constant.q * (phot_flux_ic - phot_flux_vi) 512 513 @classmethod 514 def sq1961( 515 cls, 516 *, 517 bandgap_vc: Real, 518 bandwidth_vc: Real, 519 temperature: Real, 520 voltage: Real, 521 concentration: Real, 522 ) -> Self | None: 523 r"""Shockley-Queisser detailed balance model. 524 525 Note: 526 See [original article](https://dx.doi.org/10.1063%2F1.1736034) for 527 details. 528 529 Args: 530 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 531 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 532 temperature: Temperature [$\mathrm{K}$]. 533 voltage: Voltage [$\mathrm{V}$]. 534 concentration: Sunlight concentration factor. 535 536 Returns: 537 Detailed balance model. 538 """ 539 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.sq1961( 540 bandgap_vc=bandgap_vc, 541 bandwidth_vc=bandwidth_vc, 542 ) 543 if rsv_sp_absorb_ppoly is None: 544 return None 545 else: 546 return cls( 547 rsv_sp_absorb_ppoly, 548 temperature=temperature, 549 voltage=voltage, 550 concentration=concentration, 551 ) 552 553 @classmethod 554 def lh2008( 555 cls, 556 *, 557 bandgap_vi: Real, 558 bandgap_ic: Real, 559 bandgap_vc: Real, 560 bandwidth_ii: Real, 561 bandwidth_vi: Real, 562 bandwidth_ic: Real, 563 bandwidth_vc: Real, 564 variant: str, 565 temperature: Real, 566 voltage: Real, 567 concentration: Real, 568 ) -> Self | None: 569 r"""Levi-Honsberg detailed balance model. 570 571 Note: 572 See 573 [original article](http://dx.doi.org/10.1103%2FPhysRevB.78.165122) 574 for details. 575 576 Args: 577 bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$]. 578 bandgap_ic: Intermediate-conduction optical band gap 579 [$\mathrm{eV}$]. 580 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 581 bandwidth_ii: Intermediate-intermediate optical band width 582 [$\mathrm{eV}$]. 583 bandwidth_vi: Valence-intermediate optical band width 584 [$\mathrm{eV}$]. 585 bandwidth_ic: Intermediate-conduction optical band width 586 [$\mathrm{eV}$]. 587 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 588 variant: Model variant ('equal', 'inter', or 'intra'). 589 temperature: Temperature [$\mathrm{K}$]. 590 voltage: Voltage [$\mathrm{V}$]. 591 concentration: Sunlight concentration factor. 592 593 Returns: 594 Detailed balance model. 595 """ 596 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.lh2008( 597 bandgap_vi=bandgap_vi, 598 bandgap_ic=bandgap_ic, 599 bandgap_vc=bandgap_vc, 600 bandwidth_ii=bandwidth_ii, 601 bandwidth_vi=bandwidth_vi, 602 bandwidth_ic=bandwidth_ic, 603 bandwidth_vc=bandgap_vc, 604 ) 605 if rsv_sp_absorb_ppoly is None: 606 return None 607 else: 608 return cls( 609 rsv_sp_absorb_ppoly, 610 temperature=temperature, 611 voltage=voltage, 612 concentration=concentration, 613 ) 614 615 @classmethod 616 def from_data( 617 cls, 618 energy: Sequence[Real, ...], 619 rsv_sp_absorb: Mapping[str, Sequence[Real, ...]], 620 *, 621 temperature: Real, 622 voltage: Real, 623 concentration: Real, 624 ) -> Self: 625 r"""Create detailed balance model from data. 626 627 Args: 628 energy: Photon energy values [$\mathrm{eV}$]. 629 rsv_sp_absorb: Mapping of band pair labels ('ii', 'vi', 'ic', 'vc') 630 into corresponding spectral absorbance values. 631 temperature: Temperature [$\mathrm{K}$]. 632 voltage: Voltage [$\mathrm{V}$]. 633 concentration: Sunlight concentration factor. 634 635 Returns: 636 Detailed balance model. 637 """ 638 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.from_data( 639 energy, 640 rsv_sp_absorb, 641 ) 642 if rsv_sp_absorb_ppoly is None: 643 return None 644 else: 645 return cls( 646 rsv_sp_absorb_ppoly, 647 temperature=temperature, 648 voltage=voltage, 649 concentration=concentration, 650 )
17class RadiativeExchangeChannel: 18 r"""Radiative exchange channel. 19 20 Args: 21 sp_absorb_ppoly: Spectral absorbance piecewise polynomial. 22 temperature: Temperature [$\mathrm{K}$]. 23 potential: Chemical potential [$\mathrm{eV}$]. 24 concentration: Sunlight concentration factor. 25 26 Raises: 27 TypeError: If sp_absorb_ppoly is not a SpectralAbsorbancePPoly object. 28 ValueError: If temperature, chemical potential, or sunlight 29 concentration factor are unphysical. 30 """ 31 32 def __init__( 33 self, 34 sp_absorb_ppoly: SpectralAbsorbancePPoly, 35 *, 36 temperature: Real, 37 potential: Real, 38 concentration: Real, 39 ) -> None: 40 r"""Initialize RadiativeExchangeChannel object.""" 41 self.sp_absorb_ppoly = sp_absorb_ppoly 42 self.temperature = temperature 43 self.potential = potential 44 self.concentration = concentration 45 46 @property 47 def sp_absorb_ppoly(self) -> SpectralAbsorbancePPoly: 48 r"""Spectral absorbance piecewise polynomial.""" 49 return self._sp_absorb_ppoly 50 51 @sp_absorb_ppoly.setter 52 def sp_absorb_ppoly(self, arg: SpectralAbsorbancePPoly) -> None: 53 if not isinstance(arg, SpectralAbsorbancePPoly): 54 raise TypeError("Not a SpectralAbsorbancePPoly object!") 55 self._sp_absorb_ppoly = arg 56 self.clear_cached_properties() 57 58 @property 59 def temperature(self) -> float: 60 r"""Temperature [$\mathrm{K}$].""" 61 return self._temperature 62 63 @temperature.setter 64 def temperature(self, arg: Real) -> None: 65 if not isinstance(arg, Real) or not arg > 0.0: 66 raise ValueError("Temperature must be a positive number!") 67 self._temperature = float(arg) 68 self.clear_cached_properties() 69 70 @property 71 def potential(self) -> float: 72 r"""Chemical potential [$\mathrm{eV}$].""" 73 return self._potential 74 75 @potential.setter 76 def potential(self, arg: Real) -> None: 77 if not isinstance(arg, Real): 78 raise ValueError("Chemical potential must be a real number!") 79 self._potential = float(arg) 80 self.clear_cached_properties() 81 82 @property 83 def concentration(self) -> float: 84 r"""Sunlight concentration factor.""" 85 return self._concentration 86 87 @concentration.setter 88 def concentration(self, arg: Real) -> None: 89 if not isinstance(arg, Real) or not (0.0 <= arg <= 46200.0): 90 raise ValueError( 91 "Sunlight concentration factor must be between 0 and 46200!" 92 ) 93 self._concentration = float(arg) 94 self.clear_cached_properties() 95 96 @property 97 def phot_flux(self) -> float: 98 r"""Photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 99 if self._phot_flux is None: 100 self._phot_flux = 0.0 101 if not np.isnan(self.sp_absorb_ppoly.onset): 102 for i, pcoeff_i in enumerate(self.sp_absorb_ppoly.pcoeff.T): 103 energy_lb = self.sp_absorb_ppoly.energy[i] 104 energy_ub = self.sp_absorb_ppoly.energy[i + 1] 105 for j, pcoeff_ij in enumerate(pcoeff_i): 106 if pcoeff_ij != 0.0: 107 order = 2 + j 108 self._phot_flux += pcoeff_ij * ( 109 bose_einstein_integr( 110 order, 111 energy_lb, 112 temperature=self.temperature, 113 potential=self.potential, 114 ) 115 - bose_einstein_integr( 116 order, 117 energy_ub, 118 temperature=self.temperature, 119 potential=self.potential, 120 ) 121 ) 122 if self.concentration != 0.0: 123 self._phot_flux -= ( 124 pcoeff_ij 125 * self.concentration 126 * Constant.fsun 127 * ( 128 bose_einstein_integr( 129 order, 130 energy_lb, 131 temperature=Constant.tsun, 132 potential=Constant.usun, 133 ) 134 - bose_einstein_integr( 135 order, 136 energy_ub, 137 temperature=Constant.tsun, 138 potential=Constant.usun, 139 ) 140 ) 141 ) 142 return self._phot_flux 143 144 def clear_cached_properties(self) -> None: 145 r"""Clear cached properties.""" 146 self._phot_flux = None
Radiative exchange channel.
Arguments:
- sp_absorb_ppoly: Spectral absorbance piecewise polynomial.
- temperature: Temperature [$\mathrm{K}$].
- potential: Chemical potential [$\mathrm{eV}$].
- concentration: Sunlight concentration factor.
Raises:
- TypeError: If sp_absorb_ppoly is not a SpectralAbsorbancePPoly object.
- ValueError: If temperature, chemical potential, or sunlight concentration factor are unphysical.
32 def __init__( 33 self, 34 sp_absorb_ppoly: SpectralAbsorbancePPoly, 35 *, 36 temperature: Real, 37 potential: Real, 38 concentration: Real, 39 ) -> None: 40 r"""Initialize RadiativeExchangeChannel object.""" 41 self.sp_absorb_ppoly = sp_absorb_ppoly 42 self.temperature = temperature 43 self.potential = potential 44 self.concentration = concentration
Initialize RadiativeExchangeChannel object.
46 @property 47 def sp_absorb_ppoly(self) -> SpectralAbsorbancePPoly: 48 r"""Spectral absorbance piecewise polynomial.""" 49 return self._sp_absorb_ppoly
Spectral absorbance piecewise polynomial.
58 @property 59 def temperature(self) -> float: 60 r"""Temperature [$\mathrm{K}$].""" 61 return self._temperature
Temperature [$\mathrm{K}$].
70 @property 71 def potential(self) -> float: 72 r"""Chemical potential [$\mathrm{eV}$].""" 73 return self._potential
Chemical potential [$\mathrm{eV}$].
82 @property 83 def concentration(self) -> float: 84 r"""Sunlight concentration factor.""" 85 return self._concentration
Sunlight concentration factor.
96 @property 97 def phot_flux(self) -> float: 98 r"""Photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 99 if self._phot_flux is None: 100 self._phot_flux = 0.0 101 if not np.isnan(self.sp_absorb_ppoly.onset): 102 for i, pcoeff_i in enumerate(self.sp_absorb_ppoly.pcoeff.T): 103 energy_lb = self.sp_absorb_ppoly.energy[i] 104 energy_ub = self.sp_absorb_ppoly.energy[i + 1] 105 for j, pcoeff_ij in enumerate(pcoeff_i): 106 if pcoeff_ij != 0.0: 107 order = 2 + j 108 self._phot_flux += pcoeff_ij * ( 109 bose_einstein_integr( 110 order, 111 energy_lb, 112 temperature=self.temperature, 113 potential=self.potential, 114 ) 115 - bose_einstein_integr( 116 order, 117 energy_ub, 118 temperature=self.temperature, 119 potential=self.potential, 120 ) 121 ) 122 if self.concentration != 0.0: 123 self._phot_flux -= ( 124 pcoeff_ij 125 * self.concentration 126 * Constant.fsun 127 * ( 128 bose_einstein_integr( 129 order, 130 energy_lb, 131 temperature=Constant.tsun, 132 potential=Constant.usun, 133 ) 134 - bose_einstein_integr( 135 order, 136 energy_ub, 137 temperature=Constant.tsun, 138 potential=Constant.usun, 139 ) 140 ) 141 ) 142 return self._phot_flux
Photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].
149class DetailedBalanceModel: 150 r"""Solar cell detailed balance model. 151 152 Note: 153 Applicable to Shockley-Queisser and intermediate band solar cells. 154 155 Args: 156 rsv_sp_absorb_ppoly: Resolved spectral absorbance piecewise polynomials. 157 temperature: Temperature [$\mathrm{K}$]. 158 voltage: Voltage [$\mathrm{V}$]. 159 concentration: Sunlight concentration factor. 160 161 Raises: 162 TypeError: If rsv_sp_absorb_ppoly is not a 163 ResolvedSpectralAbsorbancePPoly object. 164 ValueError: If temperature, voltage, or sunlight concentration factor 165 are unphysical. 166 """ 167 168 def __init__( 169 self, 170 rsv_sp_absorb_ppoly: ResolvedSpectralAbsorbancePPoly, 171 *, 172 temperature: Real, 173 voltage: Real, 174 concentration: Real, 175 ) -> None: 176 r"""Initialize RadiativeExchangeSystem object.""" 177 self.rsv_sp_absorb_ppoly = rsv_sp_absorb_ppoly 178 self.temperature = temperature 179 self.voltage = voltage 180 self.concentration = concentration 181 182 @property 183 def rsv_sp_absorb_ppoly(self) -> ResolvedSpectralAbsorbancePPoly: 184 r"""Resolved spectral absorbance piecewise polynomials.""" 185 return self._rsv_sp_absorb_ppoly 186 187 @rsv_sp_absorb_ppoly.setter 188 def rsv_sp_absorb_ppoly(self, arg: ResolvedSpectralAbsorbancePPoly) -> None: 189 if not isinstance(arg, ResolvedSpectralAbsorbancePPoly): 190 raise TypeError("Not a ResolvedSpectralAbsorbancePPoly object!") 191 self._rsv_sp_absorb_ppoly = arg 192 193 @property 194 def temperature(self) -> float: 195 r"""Temperature [$\mathrm{K}$].""" 196 return self._temperature 197 198 @temperature.setter 199 def temperature(self, arg: Real) -> None: 200 if not isinstance(arg, Real) or not arg > 0.0: 201 raise ValueError("Temperature must be a positive number!") 202 self._temperature = float(arg) 203 self.clear_cached_properties() 204 205 @property 206 def voltage(self) -> float: 207 r"""Voltage [$\mathrm{V}$].""" 208 return self._voltage 209 210 @voltage.setter 211 def voltage(self, arg: Real) -> None: 212 if not isinstance(arg, Real): 213 raise ValueError("Voltage must be a real number!") 214 self._voltage = float(arg) 215 self.clear_cached_properties() 216 217 @property 218 def concentration(self) -> float: 219 r"""Sunlight concentration factor.""" 220 return self._concentration 221 222 @concentration.setter 223 def concentration(self, arg: Real) -> None: 224 if not isinstance(arg, Real) or not (0.0 <= arg <= 46200.0): 225 raise ValueError( 226 "Sunlight concentration factor must be between 0 and 46200!" 227 ) 228 self._concentration = float(arg) 229 self.clear_cached_properties() 230 231 @property 232 def voltage_ub(self) -> float: 233 r"""Voltage upper bound [$\mathrm{V}$].""" 234 if self._voltage_ub is None: 235 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 236 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 237 if ( 238 sp_absorb_vi is not None 239 and not np.isnan(sp_absorb_vi.onset) 240 and sp_absorb_ic is not None 241 and not np.isnan(sp_absorb_ic.onset) 242 ): 243 voltage_ub = sp_absorb_vi.onset + sp_absorb_ic.onset 244 else: 245 sp_absorb_vc = self.rsv_sp_absorb_ppoly.vc 246 if sp_absorb_vc is not None and not np.isnan( 247 sp_absorb_vc.onset 248 ): 249 voltage_ub = sp_absorb_vc.onset 250 else: 251 voltage_ub = np.inf 252 self._voltage_ub = float(voltage_ub) 253 return self._voltage_ub 254 255 @property 256 def potential_vi_lb(self) -> float: 257 r"""Valence-intermediate chemical potential lower bound [$\mathrm{eV}$].""" 258 if self._potential_vi_lb is None: 259 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 260 if self.voltage < self.voltage_ub and sp_absorb_ic is not None: 261 onset_ic = sp_absorb_ic.onset 262 self._potential_vi_lb = self.voltage - onset_ic 263 else: 264 self._potential_vi_lb = np.nan 265 return self._potential_vi_lb 266 267 @property 268 def potential_vi_ub(self) -> float: 269 r"""Valence-intermediate chemical potential upper bound [$\mathrm{eV}$].""" 270 if self._potential_vi_ub is None: 271 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 272 if sp_absorb_vi is not None: 273 onset_vi = sp_absorb_vi.onset 274 self._potential_vi_ub = onset_vi 275 else: 276 self._potential_vi_ub = np.nan 277 return self._potential_vi_ub 278 279 @property 280 def potential_ii(self) -> float: 281 r"""Intermediate-intermediate chemical potential [$\mathrm{eV}$].""" 282 if self._potential_ii is None: 283 if self.rsv_sp_absorb_ppoly.ii is None: 284 self._potential_ii = np.nan 285 else: 286 self._potential_ii = 0.0 287 return self._potential_ii 288 289 @property 290 def potential_vi(self) -> float: 291 r"""Valence-intermediate chemical potential [$\mathrm{eV}$].""" 292 if self._potential_vi is None: 293 if self.voltage >= self.voltage_ub: 294 self._potential_vi = np.nan 295 elif np.isnan(self.potential_vi_lb) and np.isnan( 296 self.potential_vi_ub 297 ): 298 self._potential_vi = np.nan 299 else: 300 if np.isnan(self.potential_vi_lb): 301 potential_vi_b = self.potential_vi_ub 302 potential_vi_a = potential_vi_b - 0.1 303 curr_den_ib_a = self.curr_den_ib_vs_potential_vi( 304 potential_vi_a 305 ) 306 while curr_den_ib_a <= 0.0: 307 potential_vi_a -= 0.1 308 curr_den_ib_a = self.curr_den_ib_vs_potential_vi( 309 potential_vi_a 310 ) 311 elif np.isnan(self.potential_vi_ub): 312 potential_vi_a = self.potential_vi_lb 313 potential_vi_b = potential_vi_a + 0.1 314 curr_den_ib_b = self.curr_den_ib_vs_potential_vi( 315 potential_vi_b 316 ) 317 while curr_den_ib_b >= 0.0: 318 potential_vi_b += 0.1 319 curr_den_ib_b = self.curr_den_ib_vs_potential_vi( 320 potential_vi_b 321 ) 322 else: 323 potential_vi_a = self.potential_vi_lb 324 potential_vi_b = self.potential_vi_ub 325 326 def objective(potential_vi): 327 if potential_vi == potential_vi_a: 328 return np.inf 329 elif potential_vi == potential_vi_b: 330 return -np.inf 331 else: 332 return self.curr_den_ib_vs_potential_vi(potential_vi) 333 334 sol = root_scalar( 335 objective, 336 method="brentq", 337 bracket=[potential_vi_a, potential_vi_b], 338 ) 339 if not sol.converged: 340 print( 341 "Intermediate-valence chemical potential " 342 + "did not converge!" 343 ) 344 self._potential_vi = np.nan 345 else: 346 self._potential_vi = sol.root 347 348 return self._potential_vi 349 350 @property 351 def potential_ic(self) -> float: 352 r"""Intermediate-conduction chemical potential [$\mathrm{eV}$].""" 353 if self._potential_ic is None: 354 self._potential_ic = self.potential_vc - self.potential_vi 355 return self._potential_ic 356 357 @property 358 def potential_vc(self) -> float: 359 r"""Valence-conduction chemical potential [$\mathrm{eV}$].""" 360 if self._potential_vc is None: 361 if self.voltage >= self.voltage_ub: 362 self._potential_vc = np.nan 363 else: 364 self._potential_vc = self.voltage 365 return self._potential_vc 366 367 @property 368 def phot_flux_ii(self) -> float: 369 r"""Intermediate-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 370 if self._phot_flux_ii is None: 371 if self.rsv_sp_absorb_ppoly.ii is None: 372 self._phot_flux_ii = 0.0 373 else: 374 self._phot_flux_ii = RadiativeExchangeChannel( 375 self.rsv_sp_absorb_ppoly.ii, 376 temperature=self.temperature, 377 potential=self.potential_ii, 378 concentration=self.concentration, 379 ).phot_flux 380 return self._phot_flux_ii 381 382 @property 383 def phot_flux_vi(self) -> float: 384 r"""Valence-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 385 if self._phot_flux_vi is None: 386 self._phot_flux_vi = self.phot_flux_vi_vs_potential_vi( 387 self.potential_vi 388 ) 389 return self._phot_flux_vi 390 391 @property 392 def phot_flux_ic(self) -> float: 393 r"""Intermediate-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 394 if self._phot_flux_ic is None: 395 self._phot_flux_ic = self.phot_flux_ic_vs_potential_vi( 396 self.potential_vi 397 ) 398 return self._phot_flux_ic 399 400 @property 401 def phot_flux_vc(self) -> float: 402 r"""Valence-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 403 if self._phot_flux_vc is None: 404 if self.rsv_sp_absorb_ppoly.vc is None: 405 self._phot_flux_vc = 0.0 406 else: 407 self._phot_flux_vc = RadiativeExchangeChannel( 408 self.rsv_sp_absorb_ppoly.vc, 409 temperature=self.temperature, 410 potential=self.potential_vc, 411 concentration=self.concentration, 412 ).phot_flux 413 return self._phot_flux_vc 414 415 @property 416 def curr_den_ib(self) -> float: 417 r"""Intermediate-band current density [$\mathrm{mA \, cm^{-2}}$].""" 418 if self._curr_den_ib is None: 419 self._curr_den_ib = Constant.q * ( 420 self.phot_flux_ic - self.phot_flux_vi 421 ) 422 return self._curr_den_ib 423 424 @property 425 def curr_den_vb(self) -> float: 426 r"""Valence band current density [$\mathrm{mA \, cm^{-2}}$].""" 427 if self._curr_den_vb is None: 428 self._curr_den_vb = Constant.q * ( 429 self.phot_flux_vc + self.phot_flux_vi 430 ) 431 return self._curr_den_vb 432 433 @property 434 def curr_den_cb(self) -> float: 435 r"""Conduction band current density [$\mathrm{mA \, cm^{-2}}$].""" 436 if self._curr_den_cb is None: 437 self._curr_den_cb = Constant.q * ( 438 self.phot_flux_vc + self.phot_flux_ic 439 ) 440 return self._curr_den_cb 441 442 def clear_cached_properties(self) -> None: 443 r"""Clear cached properties.""" 444 self._voltage_ub = None 445 self._potential_vi_lb = None 446 self._potential_vi_ub = None 447 self._potential_ii = None 448 self._potential_vi = None 449 self._potential_ic = None 450 self._potential_vc = None 451 self._phot_flux_ii = None 452 self._phot_flux_vi = None 453 self._phot_flux_ic = None 454 self._phot_flux_vc = None 455 self._curr_den_ib = None 456 self._curr_den_vb = None 457 self._curr_den_cb = None 458 459 def phot_flux_vi_vs_potential_vi(self, potential_vi: Real) -> float: 460 r"""Valence-intermediate photon flux density vs chemical potential. 461 462 Args: 463 potential_vi: Valence-intermediate chemical potential 464 [$\mathrm{eV}$]. 465 466 Returns: 467 Valence-intermediate photon flux density 468 [$\mathrm{s^{-1} \, cm^{-2}}$]. 469 """ 470 if self.rsv_sp_absorb_ppoly.vi is None: 471 return 0.0 472 else: 473 return RadiativeExchangeChannel( 474 self.rsv_sp_absorb_ppoly.vi, 475 temperature=self.temperature, 476 potential=potential_vi, 477 concentration=self.concentration, 478 ).phot_flux 479 480 def phot_flux_ic_vs_potential_vi(self, potential_vi: Real) -> float: 481 r"""Intermediate-conduction photon flux density vs valence-intermediate chemical potential. 482 483 Args: 484 potential_vi: Valence-intermediate chemical potential [$\mathrm{eV}$]. 485 486 Returns: 487 Intermediate-conduction photon flux density 488 [$\mathrm{s^{-1} \, cm^{-2}}$]. 489 """ 490 if self.rsv_sp_absorb_ppoly.ic is None: 491 return 0.0 492 else: 493 return RadiativeExchangeChannel( 494 self.rsv_sp_absorb_ppoly.ic, 495 temperature=self.temperature, 496 potential=self.potential_vc - potential_vi, 497 concentration=self.concentration, 498 ).phot_flux 499 500 def curr_den_ib_vs_potential_vi(self, potential_vi: Real): 501 r"""Intermediate band current density vs vi chemical potential. 502 503 Args: 504 potential_vi: Valence-intermediate chemical potential 505 [$\mathrm{eV}$]. 506 507 Returns: 508 Intermediate band current density [$\mathrm{mA \, cm^{-2}}$]. 509 """ 510 phot_flux_vi = self.phot_flux_vi_vs_potential_vi(potential_vi) 511 phot_flux_ic = self.phot_flux_ic_vs_potential_vi(potential_vi) 512 return Constant.q * (phot_flux_ic - phot_flux_vi) 513 514 @classmethod 515 def sq1961( 516 cls, 517 *, 518 bandgap_vc: Real, 519 bandwidth_vc: Real, 520 temperature: Real, 521 voltage: Real, 522 concentration: Real, 523 ) -> Self | None: 524 r"""Shockley-Queisser detailed balance model. 525 526 Note: 527 See [original article](https://dx.doi.org/10.1063%2F1.1736034) for 528 details. 529 530 Args: 531 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 532 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 533 temperature: Temperature [$\mathrm{K}$]. 534 voltage: Voltage [$\mathrm{V}$]. 535 concentration: Sunlight concentration factor. 536 537 Returns: 538 Detailed balance model. 539 """ 540 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.sq1961( 541 bandgap_vc=bandgap_vc, 542 bandwidth_vc=bandwidth_vc, 543 ) 544 if rsv_sp_absorb_ppoly is None: 545 return None 546 else: 547 return cls( 548 rsv_sp_absorb_ppoly, 549 temperature=temperature, 550 voltage=voltage, 551 concentration=concentration, 552 ) 553 554 @classmethod 555 def lh2008( 556 cls, 557 *, 558 bandgap_vi: Real, 559 bandgap_ic: Real, 560 bandgap_vc: Real, 561 bandwidth_ii: Real, 562 bandwidth_vi: Real, 563 bandwidth_ic: Real, 564 bandwidth_vc: Real, 565 variant: str, 566 temperature: Real, 567 voltage: Real, 568 concentration: Real, 569 ) -> Self | None: 570 r"""Levi-Honsberg detailed balance model. 571 572 Note: 573 See 574 [original article](http://dx.doi.org/10.1103%2FPhysRevB.78.165122) 575 for details. 576 577 Args: 578 bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$]. 579 bandgap_ic: Intermediate-conduction optical band gap 580 [$\mathrm{eV}$]. 581 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 582 bandwidth_ii: Intermediate-intermediate optical band width 583 [$\mathrm{eV}$]. 584 bandwidth_vi: Valence-intermediate optical band width 585 [$\mathrm{eV}$]. 586 bandwidth_ic: Intermediate-conduction optical band width 587 [$\mathrm{eV}$]. 588 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 589 variant: Model variant ('equal', 'inter', or 'intra'). 590 temperature: Temperature [$\mathrm{K}$]. 591 voltage: Voltage [$\mathrm{V}$]. 592 concentration: Sunlight concentration factor. 593 594 Returns: 595 Detailed balance model. 596 """ 597 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.lh2008( 598 bandgap_vi=bandgap_vi, 599 bandgap_ic=bandgap_ic, 600 bandgap_vc=bandgap_vc, 601 bandwidth_ii=bandwidth_ii, 602 bandwidth_vi=bandwidth_vi, 603 bandwidth_ic=bandwidth_ic, 604 bandwidth_vc=bandgap_vc, 605 ) 606 if rsv_sp_absorb_ppoly is None: 607 return None 608 else: 609 return cls( 610 rsv_sp_absorb_ppoly, 611 temperature=temperature, 612 voltage=voltage, 613 concentration=concentration, 614 ) 615 616 @classmethod 617 def from_data( 618 cls, 619 energy: Sequence[Real, ...], 620 rsv_sp_absorb: Mapping[str, Sequence[Real, ...]], 621 *, 622 temperature: Real, 623 voltage: Real, 624 concentration: Real, 625 ) -> Self: 626 r"""Create detailed balance model from data. 627 628 Args: 629 energy: Photon energy values [$\mathrm{eV}$]. 630 rsv_sp_absorb: Mapping of band pair labels ('ii', 'vi', 'ic', 'vc') 631 into corresponding spectral absorbance values. 632 temperature: Temperature [$\mathrm{K}$]. 633 voltage: Voltage [$\mathrm{V}$]. 634 concentration: Sunlight concentration factor. 635 636 Returns: 637 Detailed balance model. 638 """ 639 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.from_data( 640 energy, 641 rsv_sp_absorb, 642 ) 643 if rsv_sp_absorb_ppoly is None: 644 return None 645 else: 646 return cls( 647 rsv_sp_absorb_ppoly, 648 temperature=temperature, 649 voltage=voltage, 650 concentration=concentration, 651 )
Solar cell detailed balance model.
Note:
Applicable to Shockley-Queisser and intermediate band solar cells.
Arguments:
- rsv_sp_absorb_ppoly: Resolved spectral absorbance piecewise polynomials.
- temperature: Temperature [$\mathrm{K}$].
- voltage: Voltage [$\mathrm{V}$].
- concentration: Sunlight concentration factor.
Raises:
- TypeError: If rsv_sp_absorb_ppoly is not a ResolvedSpectralAbsorbancePPoly object.
- ValueError: If temperature, voltage, or sunlight concentration factor are unphysical.
168 def __init__( 169 self, 170 rsv_sp_absorb_ppoly: ResolvedSpectralAbsorbancePPoly, 171 *, 172 temperature: Real, 173 voltage: Real, 174 concentration: Real, 175 ) -> None: 176 r"""Initialize RadiativeExchangeSystem object.""" 177 self.rsv_sp_absorb_ppoly = rsv_sp_absorb_ppoly 178 self.temperature = temperature 179 self.voltage = voltage 180 self.concentration = concentration
Initialize RadiativeExchangeSystem object.
182 @property 183 def rsv_sp_absorb_ppoly(self) -> ResolvedSpectralAbsorbancePPoly: 184 r"""Resolved spectral absorbance piecewise polynomials.""" 185 return self._rsv_sp_absorb_ppoly
Resolved spectral absorbance piecewise polynomials.
193 @property 194 def temperature(self) -> float: 195 r"""Temperature [$\mathrm{K}$].""" 196 return self._temperature
Temperature [$\mathrm{K}$].
205 @property 206 def voltage(self) -> float: 207 r"""Voltage [$\mathrm{V}$].""" 208 return self._voltage
Voltage [$\mathrm{V}$].
217 @property 218 def concentration(self) -> float: 219 r"""Sunlight concentration factor.""" 220 return self._concentration
Sunlight concentration factor.
231 @property 232 def voltage_ub(self) -> float: 233 r"""Voltage upper bound [$\mathrm{V}$].""" 234 if self._voltage_ub is None: 235 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 236 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 237 if ( 238 sp_absorb_vi is not None 239 and not np.isnan(sp_absorb_vi.onset) 240 and sp_absorb_ic is not None 241 and not np.isnan(sp_absorb_ic.onset) 242 ): 243 voltage_ub = sp_absorb_vi.onset + sp_absorb_ic.onset 244 else: 245 sp_absorb_vc = self.rsv_sp_absorb_ppoly.vc 246 if sp_absorb_vc is not None and not np.isnan( 247 sp_absorb_vc.onset 248 ): 249 voltage_ub = sp_absorb_vc.onset 250 else: 251 voltage_ub = np.inf 252 self._voltage_ub = float(voltage_ub) 253 return self._voltage_ub
Voltage upper bound [$\mathrm{V}$].
255 @property 256 def potential_vi_lb(self) -> float: 257 r"""Valence-intermediate chemical potential lower bound [$\mathrm{eV}$].""" 258 if self._potential_vi_lb is None: 259 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 260 if self.voltage < self.voltage_ub and sp_absorb_ic is not None: 261 onset_ic = sp_absorb_ic.onset 262 self._potential_vi_lb = self.voltage - onset_ic 263 else: 264 self._potential_vi_lb = np.nan 265 return self._potential_vi_lb
Valence-intermediate chemical potential lower bound [$\mathrm{eV}$].
267 @property 268 def potential_vi_ub(self) -> float: 269 r"""Valence-intermediate chemical potential upper bound [$\mathrm{eV}$].""" 270 if self._potential_vi_ub is None: 271 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 272 if sp_absorb_vi is not None: 273 onset_vi = sp_absorb_vi.onset 274 self._potential_vi_ub = onset_vi 275 else: 276 self._potential_vi_ub = np.nan 277 return self._potential_vi_ub
Valence-intermediate chemical potential upper bound [$\mathrm{eV}$].
279 @property 280 def potential_ii(self) -> float: 281 r"""Intermediate-intermediate chemical potential [$\mathrm{eV}$].""" 282 if self._potential_ii is None: 283 if self.rsv_sp_absorb_ppoly.ii is None: 284 self._potential_ii = np.nan 285 else: 286 self._potential_ii = 0.0 287 return self._potential_ii
Intermediate-intermediate chemical potential [$\mathrm{eV}$].
289 @property 290 def potential_vi(self) -> float: 291 r"""Valence-intermediate chemical potential [$\mathrm{eV}$].""" 292 if self._potential_vi is None: 293 if self.voltage >= self.voltage_ub: 294 self._potential_vi = np.nan 295 elif np.isnan(self.potential_vi_lb) and np.isnan( 296 self.potential_vi_ub 297 ): 298 self._potential_vi = np.nan 299 else: 300 if np.isnan(self.potential_vi_lb): 301 potential_vi_b = self.potential_vi_ub 302 potential_vi_a = potential_vi_b - 0.1 303 curr_den_ib_a = self.curr_den_ib_vs_potential_vi( 304 potential_vi_a 305 ) 306 while curr_den_ib_a <= 0.0: 307 potential_vi_a -= 0.1 308 curr_den_ib_a = self.curr_den_ib_vs_potential_vi( 309 potential_vi_a 310 ) 311 elif np.isnan(self.potential_vi_ub): 312 potential_vi_a = self.potential_vi_lb 313 potential_vi_b = potential_vi_a + 0.1 314 curr_den_ib_b = self.curr_den_ib_vs_potential_vi( 315 potential_vi_b 316 ) 317 while curr_den_ib_b >= 0.0: 318 potential_vi_b += 0.1 319 curr_den_ib_b = self.curr_den_ib_vs_potential_vi( 320 potential_vi_b 321 ) 322 else: 323 potential_vi_a = self.potential_vi_lb 324 potential_vi_b = self.potential_vi_ub 325 326 def objective(potential_vi): 327 if potential_vi == potential_vi_a: 328 return np.inf 329 elif potential_vi == potential_vi_b: 330 return -np.inf 331 else: 332 return self.curr_den_ib_vs_potential_vi(potential_vi) 333 334 sol = root_scalar( 335 objective, 336 method="brentq", 337 bracket=[potential_vi_a, potential_vi_b], 338 ) 339 if not sol.converged: 340 print( 341 "Intermediate-valence chemical potential " 342 + "did not converge!" 343 ) 344 self._potential_vi = np.nan 345 else: 346 self._potential_vi = sol.root 347 348 return self._potential_vi
Valence-intermediate chemical potential [$\mathrm{eV}$].
350 @property 351 def potential_ic(self) -> float: 352 r"""Intermediate-conduction chemical potential [$\mathrm{eV}$].""" 353 if self._potential_ic is None: 354 self._potential_ic = self.potential_vc - self.potential_vi 355 return self._potential_ic
Intermediate-conduction chemical potential [$\mathrm{eV}$].
357 @property 358 def potential_vc(self) -> float: 359 r"""Valence-conduction chemical potential [$\mathrm{eV}$].""" 360 if self._potential_vc is None: 361 if self.voltage >= self.voltage_ub: 362 self._potential_vc = np.nan 363 else: 364 self._potential_vc = self.voltage 365 return self._potential_vc
Valence-conduction chemical potential [$\mathrm{eV}$].
367 @property 368 def phot_flux_ii(self) -> float: 369 r"""Intermediate-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 370 if self._phot_flux_ii is None: 371 if self.rsv_sp_absorb_ppoly.ii is None: 372 self._phot_flux_ii = 0.0 373 else: 374 self._phot_flux_ii = RadiativeExchangeChannel( 375 self.rsv_sp_absorb_ppoly.ii, 376 temperature=self.temperature, 377 potential=self.potential_ii, 378 concentration=self.concentration, 379 ).phot_flux 380 return self._phot_flux_ii
Intermediate-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].
382 @property 383 def phot_flux_vi(self) -> float: 384 r"""Valence-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 385 if self._phot_flux_vi is None: 386 self._phot_flux_vi = self.phot_flux_vi_vs_potential_vi( 387 self.potential_vi 388 ) 389 return self._phot_flux_vi
Valence-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].
391 @property 392 def phot_flux_ic(self) -> float: 393 r"""Intermediate-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 394 if self._phot_flux_ic is None: 395 self._phot_flux_ic = self.phot_flux_ic_vs_potential_vi( 396 self.potential_vi 397 ) 398 return self._phot_flux_ic
Intermediate-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].
400 @property 401 def phot_flux_vc(self) -> float: 402 r"""Valence-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].""" 403 if self._phot_flux_vc is None: 404 if self.rsv_sp_absorb_ppoly.vc is None: 405 self._phot_flux_vc = 0.0 406 else: 407 self._phot_flux_vc = RadiativeExchangeChannel( 408 self.rsv_sp_absorb_ppoly.vc, 409 temperature=self.temperature, 410 potential=self.potential_vc, 411 concentration=self.concentration, 412 ).phot_flux 413 return self._phot_flux_vc
Valence-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].
415 @property 416 def curr_den_ib(self) -> float: 417 r"""Intermediate-band current density [$\mathrm{mA \, cm^{-2}}$].""" 418 if self._curr_den_ib is None: 419 self._curr_den_ib = Constant.q * ( 420 self.phot_flux_ic - self.phot_flux_vi 421 ) 422 return self._curr_den_ib
Intermediate-band current density [$\mathrm{mA \, cm^{-2}}$].
424 @property 425 def curr_den_vb(self) -> float: 426 r"""Valence band current density [$\mathrm{mA \, cm^{-2}}$].""" 427 if self._curr_den_vb is None: 428 self._curr_den_vb = Constant.q * ( 429 self.phot_flux_vc + self.phot_flux_vi 430 ) 431 return self._curr_den_vb
Valence band current density [$\mathrm{mA \, cm^{-2}}$].
433 @property 434 def curr_den_cb(self) -> float: 435 r"""Conduction band current density [$\mathrm{mA \, cm^{-2}}$].""" 436 if self._curr_den_cb is None: 437 self._curr_den_cb = Constant.q * ( 438 self.phot_flux_vc + self.phot_flux_ic 439 ) 440 return self._curr_den_cb
Conduction band current density [$\mathrm{mA \, cm^{-2}}$].
442 def clear_cached_properties(self) -> None: 443 r"""Clear cached properties.""" 444 self._voltage_ub = None 445 self._potential_vi_lb = None 446 self._potential_vi_ub = None 447 self._potential_ii = None 448 self._potential_vi = None 449 self._potential_ic = None 450 self._potential_vc = None 451 self._phot_flux_ii = None 452 self._phot_flux_vi = None 453 self._phot_flux_ic = None 454 self._phot_flux_vc = None 455 self._curr_den_ib = None 456 self._curr_den_vb = None 457 self._curr_den_cb = None
Clear cached properties.
459 def phot_flux_vi_vs_potential_vi(self, potential_vi: Real) -> float: 460 r"""Valence-intermediate photon flux density vs chemical potential. 461 462 Args: 463 potential_vi: Valence-intermediate chemical potential 464 [$\mathrm{eV}$]. 465 466 Returns: 467 Valence-intermediate photon flux density 468 [$\mathrm{s^{-1} \, cm^{-2}}$]. 469 """ 470 if self.rsv_sp_absorb_ppoly.vi is None: 471 return 0.0 472 else: 473 return RadiativeExchangeChannel( 474 self.rsv_sp_absorb_ppoly.vi, 475 temperature=self.temperature, 476 potential=potential_vi, 477 concentration=self.concentration, 478 ).phot_flux
Valence-intermediate photon flux density vs chemical potential.
Arguments:
- potential_vi: Valence-intermediate chemical potential [$\mathrm{eV}$].
Returns:
Valence-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].
480 def phot_flux_ic_vs_potential_vi(self, potential_vi: Real) -> float: 481 r"""Intermediate-conduction photon flux density vs valence-intermediate chemical potential. 482 483 Args: 484 potential_vi: Valence-intermediate chemical potential [$\mathrm{eV}$]. 485 486 Returns: 487 Intermediate-conduction photon flux density 488 [$\mathrm{s^{-1} \, cm^{-2}}$]. 489 """ 490 if self.rsv_sp_absorb_ppoly.ic is None: 491 return 0.0 492 else: 493 return RadiativeExchangeChannel( 494 self.rsv_sp_absorb_ppoly.ic, 495 temperature=self.temperature, 496 potential=self.potential_vc - potential_vi, 497 concentration=self.concentration, 498 ).phot_flux
Intermediate-conduction photon flux density vs valence-intermediate chemical potential.
Arguments:
- potential_vi: Valence-intermediate chemical potential [$\mathrm{eV}$].
Returns:
Intermediate-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].
500 def curr_den_ib_vs_potential_vi(self, potential_vi: Real): 501 r"""Intermediate band current density vs vi chemical potential. 502 503 Args: 504 potential_vi: Valence-intermediate chemical potential 505 [$\mathrm{eV}$]. 506 507 Returns: 508 Intermediate band current density [$\mathrm{mA \, cm^{-2}}$]. 509 """ 510 phot_flux_vi = self.phot_flux_vi_vs_potential_vi(potential_vi) 511 phot_flux_ic = self.phot_flux_ic_vs_potential_vi(potential_vi) 512 return Constant.q * (phot_flux_ic - phot_flux_vi)
Intermediate band current density vs vi chemical potential.
Arguments:
- potential_vi: Valence-intermediate chemical potential [$\mathrm{eV}$].
Returns:
Intermediate band current density [$\mathrm{mA \, cm^{-2}}$].
514 @classmethod 515 def sq1961( 516 cls, 517 *, 518 bandgap_vc: Real, 519 bandwidth_vc: Real, 520 temperature: Real, 521 voltage: Real, 522 concentration: Real, 523 ) -> Self | None: 524 r"""Shockley-Queisser detailed balance model. 525 526 Note: 527 See [original article](https://dx.doi.org/10.1063%2F1.1736034) for 528 details. 529 530 Args: 531 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 532 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 533 temperature: Temperature [$\mathrm{K}$]. 534 voltage: Voltage [$\mathrm{V}$]. 535 concentration: Sunlight concentration factor. 536 537 Returns: 538 Detailed balance model. 539 """ 540 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.sq1961( 541 bandgap_vc=bandgap_vc, 542 bandwidth_vc=bandwidth_vc, 543 ) 544 if rsv_sp_absorb_ppoly is None: 545 return None 546 else: 547 return cls( 548 rsv_sp_absorb_ppoly, 549 temperature=temperature, 550 voltage=voltage, 551 concentration=concentration, 552 )
Shockley-Queisser detailed balance model.
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}$].
- temperature: Temperature [$\mathrm{K}$].
- voltage: Voltage [$\mathrm{V}$].
- concentration: Sunlight concentration factor.
Returns:
Detailed balance model.
554 @classmethod 555 def lh2008( 556 cls, 557 *, 558 bandgap_vi: Real, 559 bandgap_ic: Real, 560 bandgap_vc: Real, 561 bandwidth_ii: Real, 562 bandwidth_vi: Real, 563 bandwidth_ic: Real, 564 bandwidth_vc: Real, 565 variant: str, 566 temperature: Real, 567 voltage: Real, 568 concentration: Real, 569 ) -> Self | None: 570 r"""Levi-Honsberg detailed balance model. 571 572 Note: 573 See 574 [original article](http://dx.doi.org/10.1103%2FPhysRevB.78.165122) 575 for details. 576 577 Args: 578 bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$]. 579 bandgap_ic: Intermediate-conduction optical band gap 580 [$\mathrm{eV}$]. 581 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 582 bandwidth_ii: Intermediate-intermediate optical band width 583 [$\mathrm{eV}$]. 584 bandwidth_vi: Valence-intermediate optical band width 585 [$\mathrm{eV}$]. 586 bandwidth_ic: Intermediate-conduction optical band width 587 [$\mathrm{eV}$]. 588 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 589 variant: Model variant ('equal', 'inter', or 'intra'). 590 temperature: Temperature [$\mathrm{K}$]. 591 voltage: Voltage [$\mathrm{V}$]. 592 concentration: Sunlight concentration factor. 593 594 Returns: 595 Detailed balance model. 596 """ 597 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.lh2008( 598 bandgap_vi=bandgap_vi, 599 bandgap_ic=bandgap_ic, 600 bandgap_vc=bandgap_vc, 601 bandwidth_ii=bandwidth_ii, 602 bandwidth_vi=bandwidth_vi, 603 bandwidth_ic=bandwidth_ic, 604 bandwidth_vc=bandgap_vc, 605 ) 606 if rsv_sp_absorb_ppoly is None: 607 return None 608 else: 609 return cls( 610 rsv_sp_absorb_ppoly, 611 temperature=temperature, 612 voltage=voltage, 613 concentration=concentration, 614 )
Levi-Honsberg detailed balance model.
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').
- temperature: Temperature [$\mathrm{K}$].
- voltage: Voltage [$\mathrm{V}$].
- concentration: Sunlight concentration factor.
Returns:
Detailed balance model.
616 @classmethod 617 def from_data( 618 cls, 619 energy: Sequence[Real, ...], 620 rsv_sp_absorb: Mapping[str, Sequence[Real, ...]], 621 *, 622 temperature: Real, 623 voltage: Real, 624 concentration: Real, 625 ) -> Self: 626 r"""Create detailed balance model from data. 627 628 Args: 629 energy: Photon energy values [$\mathrm{eV}$]. 630 rsv_sp_absorb: Mapping of band pair labels ('ii', 'vi', 'ic', 'vc') 631 into corresponding spectral absorbance values. 632 temperature: Temperature [$\mathrm{K}$]. 633 voltage: Voltage [$\mathrm{V}$]. 634 concentration: Sunlight concentration factor. 635 636 Returns: 637 Detailed balance model. 638 """ 639 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.from_data( 640 energy, 641 rsv_sp_absorb, 642 ) 643 if rsv_sp_absorb_ppoly is None: 644 return None 645 else: 646 return cls( 647 rsv_sp_absorb_ppoly, 648 temperature=temperature, 649 voltage=voltage, 650 concentration=concentration, 651 )
Create detailed balance model from data.
Arguments:
- energy: Photon energy values [$\mathrm{eV}$].
- rsv_sp_absorb: Mapping of band pair labels ('ii', 'vi', 'ic', 'vc') into corresponding spectral absorbance values.
- temperature: Temperature [$\mathrm{K}$].
- voltage: Voltage [$\mathrm{V}$].
- concentration: Sunlight concentration factor.
Returns:
Detailed balance model.