scambio.cell
Solar cell.
1r"""Solar cell.""" 2 3import numpy as np 4import seaborn as sns 5from collections.abc import Mapping, Sequence 6from math import ceil 7from matplotlib import pyplot as plt 8from numbers import Real 9from scipy.optimize import minimize_scalar, root_scalar 10from typing import Self 11from scambio.absorbance import ResolvedSpectralAbsorbancePPoly 12from scambio.balance import DetailedBalanceModel 13from scambio.constant import Constant 14from scambio.represent import repr_num_seq 15 16 17sns.set_theme( 18 context="talk", 19 style="white", 20 rc={"figure.titlesize": "medium", "axes.formatter.useoffset": False}, 21) 22 23 24class AppliedSolarCell: 25 r"""Solar cell at given temperature and Sunlight concentration factor. 26 27 Args: 28 rsv_sp_absorb_ppoly: Resolved spectral absorbance piecewise polynomials. 29 temperature: Temperature [$\mathrm{K}$]. 30 concentration: Sunlight concentration factor. 31 """ 32 33 def __init__( 34 self, 35 rsv_sp_absorb_ppoly: ResolvedSpectralAbsorbancePPoly, 36 *, 37 temperature: Real, 38 concentration: Real, 39 ) -> None: 40 r"""Initialize AppliedSolarCell object.""" 41 self.rsv_sp_absorb_ppoly = rsv_sp_absorb_ppoly 42 self.temperature = temperature 43 self.concentration = concentration 44 self.clear_voltage_sweep() 45 46 def __repr__(self) -> str: 47 r"""Represent AppliedSolarCell object.""" 48 return "\n".join( 49 [ 50 "Applied solar cell:", 51 f"- ii band / eV: [{self.onset_ii:.2f}, {self.offset_ii:.2f}]", 52 f"- vi band / eV: [{self.onset_vi:.2f}, {self.offset_vi:.2f}]", 53 f"- ic band / eV: [{self.onset_ic:.2f}, {self.offset_ic:.2f}]", 54 f"- vc band / eV: [{self.onset_vc:.2f}, {self.offset_vc:.2f}]", 55 f"- temperature / K: {self.temperature:.1f}", 56 f"- concentration: {self.concentration:.0f}", 57 f"- voltage sweep / V: {repr_num_seq(self.voltage_sweep, margin=22)}", 58 ] 59 ) 60 61 @property 62 def rsv_sp_absorb_ppoly(self) -> ResolvedSpectralAbsorbancePPoly: 63 r"""Resolved spectral absorbance piecewise polynomials.""" 64 return self._rsv_sp_absorb_ppoly 65 66 @rsv_sp_absorb_ppoly.setter 67 def rsv_sp_absorb_ppoly(self, arg: ResolvedSpectralAbsorbancePPoly) -> None: 68 if not isinstance(arg, ResolvedSpectralAbsorbancePPoly): 69 raise TypeError("Not a ResolvedSpectralAbsorbancePPoly object!") 70 self._rsv_sp_absorb_ppoly = arg 71 72 @property 73 def temperature(self) -> float: 74 r"""Temperature [$\mathrm{K}$].""" 75 return self._temperature 76 77 @temperature.setter 78 def temperature(self, arg: Real) -> None: 79 if not isinstance(arg, Real) or not arg > 0.0: 80 raise ValueError("Temperature must be a positive number!") 81 self._temperature = float(arg) 82 self.clear_cached_properties() 83 84 @property 85 def concentration(self) -> float: 86 r"""Sunlight concentration factor.""" 87 return self._concentration 88 89 @concentration.setter 90 def concentration(self, arg: Real) -> None: 91 if not isinstance(arg, Real) or not (0.0 <= arg <= 46200.0): 92 raise ValueError( 93 "Sunlight concentration factor must be between 0 and 46200!" 94 ) 95 self._concentration = float(arg) 96 self.clear_cached_properties() 97 98 @property 99 def onset_ii(self) -> float: 100 r"""Intermediate-intermediate onset [$\mathrm{eV}$].""" 101 if self._onset_ii is None: 102 sp_absorb_ii = self.rsv_sp_absorb_ppoly.ii 103 if sp_absorb_ii is not None: 104 self._onset_ii = sp_absorb_ii.onset 105 else: 106 self._onset_ii = np.nan 107 return self._onset_ii 108 109 @property 110 def offset_ii(self) -> float: 111 r"""Intermediate-intermediate offset [$\mathrm{eV}$].""" 112 if self._offset_ii is None: 113 sp_absorb_ii = self.rsv_sp_absorb_ppoly.ii 114 if sp_absorb_ii is not None: 115 self._offset_ii = sp_absorb_ii.offset 116 else: 117 self._offset_ii = np.nan 118 return self._offset_ii 119 120 @property 121 def onset_vi(self) -> float: 122 r"""Valence-intermediate onset [$\mathrm{eV}$].""" 123 if self._onset_vi is None: 124 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 125 if sp_absorb_vi is not None: 126 self._onset_vi = sp_absorb_vi.onset 127 else: 128 self._onset_vi = np.nan 129 return self._onset_vi 130 131 @property 132 def offset_vi(self) -> float: 133 r"""Valence-intermediate offset [$\mathrm{eV}$].""" 134 if self._offset_vi is None: 135 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 136 if sp_absorb_vi is not None: 137 self._offset_vi = sp_absorb_vi.offset 138 else: 139 self._offset_vi = np.nan 140 return self._offset_vi 141 142 @property 143 def onset_ic(self) -> float: 144 r"""Intermediate-conduction onset [$\mathrm{eV}$].""" 145 if self._onset_ic is None: 146 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 147 if sp_absorb_ic is not None: 148 self._onset_ic = sp_absorb_ic.onset 149 else: 150 self._onset_ic = np.nan 151 return self._onset_ic 152 153 @property 154 def offset_ic(self) -> float: 155 r"""Intermediate-conduction offset [$\mathrm{eV}$].""" 156 if self._offset_ic is None: 157 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 158 if sp_absorb_ic is not None: 159 self._offset_ic = sp_absorb_ic.offset 160 else: 161 self._offset_ic = np.nan 162 return self._offset_ic 163 164 @property 165 def onset_vc(self) -> float: 166 r"""Valence-conduction onset [$\mathrm{eV}$].""" 167 if self._onset_vc is None: 168 sp_absorb_vc = self.rsv_sp_absorb_ppoly.vc 169 if sp_absorb_vc is not None: 170 self._onset_vc = sp_absorb_vc.onset 171 else: 172 self._onset_vc = np.nan 173 return self._onset_vc 174 175 @property 176 def offset_vc(self) -> float: 177 r"""Valence-conduction offset [$\mathrm{eV}$].""" 178 if self._offset_vc is None: 179 sp_absorb_vc = self.rsv_sp_absorb_ppoly.vc 180 if sp_absorb_vc is not None: 181 self._offset_vc = sp_absorb_vc.offset 182 else: 183 self._offset_vc = np.nan 184 return self._offset_vc 185 186 @property 187 def voltage_ub(self) -> float: 188 r"""Voltage upper bound [$\mathrm{V}$].""" 189 if self._voltage_ub is None: 190 voltage_ub = min( 191 np.nan_to_num(self.onset_vi + self.onset_ic, nan=np.inf), 192 np.nan_to_num(self.onset_vc, nan=np.inf), 193 np.inf 194 ) 195 self._voltage_ub = float(voltage_ub) 196 return self._voltage_ub 197 198 @property 199 def voltage_oc(self) -> float: 200 r"""Open circuit voltage [$\mathrm{V}$].""" 201 if self._voltage_oc is None: 202 voltage_b = self.voltage_ub 203 voltage_a = voltage_b - 1.0 204 curr_den_a = self.curr_den_vs_voltage(voltage_a) 205 while curr_den_a > 0.0: 206 voltage_a -= 0.1 207 208 def objective(voltage): 209 if voltage == voltage_b: 210 return np.inf 211 else: 212 return self.curr_den_vs_voltage(voltage) 213 214 sol = root_scalar( 215 objective, 216 method="brentq", 217 bracket=[voltage_a, voltage_b], 218 ) 219 if not sol.converged: 220 print("Open circuit voltage did not converge!") 221 self._voltage_oc = np.nan 222 else: 223 self._voltage_oc = sol.root 224 225 return self._voltage_oc 226 227 @property 228 def curr_den_sc(self) -> float: 229 r"""Short circuit current density [$\mathrm{mA \, cm^{-2}}$].""" 230 if self._curr_den_sc is None: 231 self._curr_den_sc = self.curr_den_vs_voltage(0.0) 232 return self._curr_den_sc 233 234 @property 235 def voltage_mpp(self) -> float: 236 r"""Maximum power point voltage [$\mathrm{V}$].""" 237 if self._voltage_mpp is None: 238 res = minimize_scalar( 239 self.power_den_vs_voltage, 240 method="bounded", 241 bounds=sorted([0.0, self.voltage_oc]), 242 ) 243 if not res.success: 244 print("Maximum power point voltage did not converge!") 245 self._voltage_mpp = np.nan 246 else: 247 self._voltage_mpp = res.x 248 249 return self._voltage_mpp 250 251 @property 252 def db_model_mpp(self) -> DetailedBalanceModel: 253 r"""Maximum power point detailed balance model.""" 254 if self._db_model_mpp is None: 255 self._db_model_mpp = self.db_model_vs_voltage(self.voltage_mpp) 256 return self._db_model_mpp 257 258 @property 259 def curr_den_mpp(self) -> float: 260 r"""Maximum power point current density [$\mathrm{mA \, cm^{-2}}$].""" 261 if self._curr_den_mpp is None: 262 if abs(self.db_model_mpp.curr_den_vb) < abs( 263 self.db_model_mpp.curr_den_cb 264 ): 265 self._curr_den_mpp = self.db_model_mpp.curr_den_vb 266 else: 267 self._curr_den_mpp = self.db_model_mpp.curr_den_cb 268 return self._curr_den_mpp 269 270 @property 271 def power_den_mpp(self) -> float: 272 r"""Maximum power point power density [$\mathrm{mW \, cm^{-2}}$].""" 273 if self._power_den_mpp is None: 274 self._power_den_mpp = self.curr_den_mpp * self.voltage_mpp 275 return self._power_den_mpp 276 277 @property 278 def efficiency_mpp(self) -> float: 279 r"""Maximum power point power conversion efficiency [$\mathrm{\%}$].""" 280 if self._efficiency_mpp is None: 281 self._efficiency_mpp = ( 282 -100.0 283 * self.power_den_mpp 284 / (self.concentration * Constant.psun) 285 ) 286 return self._efficiency_mpp 287 288 @property 289 def fill_factor(self) -> float: 290 r"""Fill factor.""" 291 if self._fill_factor is None: 292 self._fill_factor = self.power_den_mpp / ( 293 self.voltage_oc * self.curr_den_sc 294 ) 295 return self._fill_factor 296 297 @property 298 def voltage_sweep(self) -> np.ndarray[float]: 299 r"""Voltage sweep [$\mathrm{V}$].""" 300 return self._voltage_sweep 301 302 @property 303 def db_model_sweep(self) -> list[DetailedBalanceModel, ...]: 304 r"""Detailed balance model sweep.""" 305 if self._db_model_sweep is None: 306 self._db_model_sweep = [ 307 self.db_model_vs_voltage(voltage) 308 for voltage in self.voltage_sweep 309 ] 310 return self._db_model_sweep 311 312 @property 313 def curr_den_sweep(self) -> np.ndarray[float]: 314 r"""Current density sweep [$\mathrm{mA \, cm^{-2}}$].""" 315 if self._curr_den_sweep is None: 316 self._curr_den_sweep = np.array([], dtype=float) 317 for db_model in self.db_model_sweep: 318 if abs(db_model.curr_den_vb) < abs(db_model.curr_den_cb): 319 self._curr_den_sweep = np.append( 320 self._curr_den_sweep, 321 db_model.curr_den_vb, 322 ) 323 else: 324 self._curr_den_sweep = np.append( 325 self._curr_den_sweep, 326 db_model.curr_den_cb, 327 ) 328 return self._curr_den_sweep 329 330 @property 331 def power_den_sweep(self) -> np.ndarray[float]: 332 r"""Power density sweep [$\mathrm{mW \, cm^{-2}}$].""" 333 if self._power_den_sweep is None: 334 self._power_den_sweep = self.curr_den_sweep * self.voltage_sweep 335 return self._power_den_sweep 336 337 @property 338 def efficiency_sweep(self) -> np.ndarray[float]: 339 r"""Power conversion efficiency sweep [$\mathrm{\%}$].""" 340 if self._efficiency_sweep is None: 341 self._efficiency_sweep = ( 342 -100.0 343 * self.power_den_sweep 344 / (self.concentration * Constant.psun) 345 ) 346 return self._efficiency_sweep 347 348 def clear_cached_properties(self) -> None: 349 r"""Clear cached properties.""" 350 self._onset_ii = None 351 self._offset_ii = None 352 self._onset_vi = None 353 self._offset_vi = None 354 self._onset_ic = None 355 self._offset_ic = None 356 self._onset_vc = None 357 self._offset_vc = None 358 self._voltage_ub = None 359 self._voltage_oc = None 360 self._curr_den_sc = None 361 self._voltage_mpp = None 362 self._db_model_mpp = None 363 self._curr_den_mpp = None 364 self._power_den_mpp = None 365 self._efficiency_mpp = None 366 self._fill_factor = None 367 self._db_model_sweep = None 368 self._curr_den_sweep = None 369 self._power_den_sweep = None 370 self._efficiency_sweep = None 371 372 def db_model_vs_voltage(self, voltage: Real) -> DetailedBalanceModel: 373 r"""Detailed balance model vs voltage. 374 375 Args: 376 voltage: Voltage [$\mathrm{V}$]. 377 378 Returns: 379 Detailed balance model. 380 """ 381 return DetailedBalanceModel( 382 self.rsv_sp_absorb_ppoly, 383 temperature=self.temperature, 384 voltage=voltage, 385 concentration=self.concentration, 386 ) 387 388 def curr_den_vs_voltage(self, voltage: Real) -> float: 389 r"""Current density vs voltage. 390 391 Note: 392 Valence and conduction band current densities should be the same. 393 However, the condition of zero intermediate band current density 394 cannot always be reached, such as in narrow-gap intermediate 395 band solar cells at high concentration. 396 This introduces a non-negligible difference between valence and 397 conduction band current densities, according to the continuity 398 equation. 399 Currently, the smaller among the two current densities is taken 400 as the solar cell current density. 401 This is particularly helpful to aid numerical optimization 402 methods. 403 404 Args: 405 voltage: Voltage [$\mathrm{V}$]. 406 407 Returns: 408 Current density [$\mathrm{mA \, cm^{-2}}$]. 409 """ 410 db_model = self.db_model_vs_voltage(voltage) 411 if abs(db_model.curr_den_vb) < abs(db_model.curr_den_cb): 412 return db_model.curr_den_vb 413 else: 414 return db_model.curr_den_cb 415 416 def power_den_vs_voltage(self, voltage: Real) -> float: 417 r"""Power density vs voltage. 418 419 Args: 420 voltage: Voltage [$\mathrm{V}$]. 421 422 Returns: 423 Power density [$\mathrm{mW \, cm^{-2}}$]. 424 """ 425 return voltage * self.curr_den_vs_voltage(voltage) 426 427 def apply_voltage_sweep(self, voltage_sweep: Sequence[Real, ...]) -> None: 428 r"""Apply voltage sweep.""" 429 self._voltage_sweep = np.unique(voltage_sweep).astype(float) 430 self._db_model_sweep = None 431 self._curr_den_sweep = None 432 self._power_den_sweep = None 433 self._efficiency_sweep = None 434 435 def clear_voltage_sweep(self) -> None: 436 r"""Clear voltage sweep.""" 437 self.apply_voltage_sweep([]) 438 439 def plot_rsv_sp_absorb_vs_energy( 440 self, 441 *, 442 energy_min: Real, 443 energy_max: Real, 444 energy_inc: Real, 445 ax: plt.Axes | None = None, 446 ) -> plt.Figure | None: 447 r"""Plot resolved spectral absorbance vs photon energy. 448 449 Args: 450 energy_min: Photon energy minimum [$\mathrm{eV}$]. 451 energy_max: Photon energy maximum [$\mathrm{eV}$]. 452 energy_inc: Photon energy increment [$\mathrm{eV}$]. 453 ax: Plot axes. If None, new figure and axes are created. 454 455 Returns: 456 Plot figure. 457 """ 458 return self.rsv_sp_absorb_ppoly.plot_rsv_sp_absorb_vs_energy( 459 energy_min=energy_min, 460 energy_max=energy_max, 461 energy_inc=energy_inc, 462 ax=ax, 463 ) 464 465 def plot_voltage_oc( 466 self, 467 *, 468 ax: plt.Axes | None = None, 469 color: str | None = "k", 470 label: str | None = None, 471 ) -> plt.Figure: 472 r"""Plot open circuit voltage. 473 474 Args: 475 ax: Plot axes. If None, new figure and axes are created. 476 color: Color. 477 label: Label. 478 479 Returns: 480 Plot figure. 481 """ 482 if ax is None: 483 fig, ax = plt.subplots(tight_layout=True) 484 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 485 else: 486 fig = ax.get_figure() 487 488 ax.axvline(self.voltage_oc, linestyle=":", color=color, label=label) 489 return fig 490 491 def plot_curr_den_sc( 492 self, 493 *, 494 ax: plt.Axes | None = None, 495 color: str | None = "k", 496 label: str | None = None, 497 ) -> plt.Figure: 498 r"""Plot short circuit current density. 499 500 Args: 501 ax: Plot axes. If None, new figure and axes are created. 502 color: Color. 503 label: Label. 504 505 Returns: 506 Plot figure. 507 """ 508 if ax is None: 509 fig, ax = plt.subplots(tight_layout=True) 510 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 511 else: 512 fig = ax.get_figure() 513 514 ax.axhline(self.curr_den_sc, linestyle=":", color=color, label=label) 515 return fig 516 517 def plot_voltage_mpp( 518 self, 519 *, 520 ax: plt.Axes | None = None, 521 color: str | None = "k", 522 label: str | None = None, 523 ) -> plt.Figure: 524 r"""Plot maximum power point voltage. 525 526 Args: 527 ax: Plot axes. If None, new figure and axes are created. 528 color: Color. 529 label: Label. 530 531 Returns: 532 Plot figure. 533 """ 534 if ax is None: 535 fig, ax = plt.subplots(tight_layout=True) 536 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 537 else: 538 fig = ax.get_figure() 539 540 ax.axvline(self.voltage_mpp, linestyle=":", color=color, label=label) 541 return fig 542 543 def plot_curr_den_mpp( 544 self, 545 *, 546 ax: plt.Axes | None = None, 547 color: str | None = "k", 548 label: str | None = None, 549 ) -> plt.Figure: 550 r"""Plot maximum power point current density. 551 552 Args: 553 ax: Plot axes. If None, new figure and axes are created. 554 color: Color. 555 label: Label. 556 557 Returns: 558 Plot figure. 559 """ 560 if ax is None: 561 fig, ax = plt.subplots(tight_layout=True) 562 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 563 else: 564 fig = ax.get_figure() 565 566 ax.axhline(self.curr_den_mpp, linestyle=":", color=color, label=label) 567 return fig 568 569 def plot_power_den_mpp( 570 self, 571 *, 572 ax: plt.Axes | None = None, 573 color: str | None = "k", 574 label: str | None = None, 575 ) -> plt.Figure: 576 r"""Plot maximum power point power density. 577 578 Args: 579 ax: Plot axes. If None, new figure and axes are created. 580 color: Color. 581 label: Label. 582 583 Returns: 584 Plot figure. 585 """ 586 if ax is None: 587 fig, ax = plt.subplots(tight_layout=True) 588 ax.set_ylabel(r"Power density / $\mathrm{mA \cdot cm^{-2}}$") 589 else: 590 fig = ax.get_figure() 591 592 ax.axhline(self.power_den_mpp, linestyle=":", color=color, label=label) 593 return fig 594 595 def plot_efficiency_mpp( 596 self, 597 *, 598 ax: plt.Axes | None = None, 599 color: str | None = "k", 600 label: str | None = None, 601 ) -> plt.Figure: 602 r"""Plot maximum power point efficiency. 603 604 Args: 605 ax: Plot axes. If None, new figure and axes are created. 606 color: Color. 607 label: Label. 608 609 Returns: 610 Plot figure. 611 """ 612 if ax is None: 613 fig, ax = plt.subplots(tight_layout=True) 614 ax.set_ylabel(r"Efficiency / $\mathrm{\%}$") 615 else: 616 fig = ax.get_figure() 617 618 ax.axhline(self.efficiency_mpp, linestyle=":", color=color, label=label) 619 return fig 620 621 def plot_curr_den_vs_voltage( 622 self, 623 *, 624 ax: plt.Axes | None = None, 625 color: str | None = None, 626 label: str | None = None, 627 ) -> plt.Figure: 628 r"""Plot current density vs voltage. 629 630 Args: 631 color: Color. 632 label: Label. 633 ax: Plot axes. If None, new figure and axes are created. 634 635 Returns: 636 Plot figure. 637 """ 638 if ax is None: 639 fig, ax = plt.subplots(tight_layout=True) 640 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 641 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 642 ax.set_ylim(ceil(self.curr_den_sc / 10) * 10 - 20, 0.0) 643 else: 644 fig = ax.get_figure() 645 646 ax.plot( 647 self.voltage_sweep, 648 self.curr_den_sweep, 649 color=color, 650 label=label, 651 ) 652 return fig 653 654 def plot_power_den_vs_voltage( 655 self, 656 *, 657 ax: plt.Axes | None = None, 658 color: str | None = None, 659 label: str | None = None, 660 ) -> plt.Figure: 661 r"""Plot power density vs voltage. 662 663 Args: 664 ax: Plot axes. If None, new figure and axes are created. 665 color: Color. 666 label: Label. 667 668 Returns: 669 Plot figure. 670 """ 671 if ax is None: 672 fig, ax = plt.subplots(tight_layout=True) 673 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 674 ax.set_ylabel(r"Power density / $\mathrm{mW \cdot cm^{-2}}$") 675 ax.set_ylim(ceil(self.power_den_mpp / 10) * 10 - 20, 0.0) 676 else: 677 fig = ax.get_figure() 678 679 ax.plot( 680 self.voltage_sweep, 681 self.power_den_sweep, 682 color=color, 683 label=label, 684 ) 685 return fig 686 687 def plot_efficiency_vs_voltage( 688 self, 689 *, 690 ax: plt.Axes | None = None, 691 color: str | None = None, 692 label: str | None = None, 693 ) -> plt.Figure: 694 r"""Plot efficiency vs voltage. 695 696 Args: 697 ax: Plot axes. If None, new figure and axes are created. 698 color: Color. 699 label: Label. 700 701 Returns: 702 Plot figure. 703 """ 704 if ax is None: 705 fig, ax = plt.subplots(tight_layout=True) 706 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 707 ax.set_ylabel(r"Efficiency / $\mathrm{\%}$") 708 ax.set_ylim(0.0, ceil(self.efficiency_mpp / 10) * 10 + 10) 709 else: 710 fig = ax.get_figure() 711 712 ax.plot( 713 self.voltage_sweep, 714 self.efficiency_sweep, 715 color=color, 716 label=label, 717 ) 718 return fig 719 720 def plot_db_model_qty_mpp( 721 self, 722 qty: str, 723 *, 724 ax: plt.Axes | None = None, 725 color: str | None = "k", 726 label: str | None = None, 727 ) -> plt.Figure: 728 r"""Plot maximum power point detailed balance model quantity. 729 730 Args: 731 qty: Quantity, among 'potential_ii', 'potential_vi', 'potential_ic', 732 'potential_vc', 'phot_flux_ii', 'phot_flux_vi', 'phot_flux_ic', 733 'phot_flux_vc', 'curr_den_ib', 'curr_den_vb', and 'curr_den_cb'. 734 ax: Plot axes. If None, new figure and axes are created. 735 color: Color. 736 label: Label. 737 738 Returns: 739 Plot figure. 740 """ 741 if qty not in [ 742 "potential_ii", 743 "potential_vi", 744 "potential_ic", 745 "potential_vc", 746 "phot_flux_ii", 747 "phot_flux_vi", 748 "phot_flux_ic", 749 "phot_flux_vc", 750 "curr_den_ib", 751 "curr_den_vb", 752 "curr_den_cb", 753 ]: 754 raise ValueError("Quantity not supported!") 755 756 if ax is None: 757 fig, ax = plt.subplots(tight_layout=True) 758 if qty in [ 759 "potential_ii", 760 "potential_vi", 761 "potential_ic", 762 "potential_vc", 763 ]: 764 ax.set_ylabel(r"Chemical potential / $\mathrm{eV}$") 765 elif qty in [ 766 "phot_flux_ii", 767 "phot_flux_vi", 768 "phot_flux_ic", 769 "phot_flux_vc", 770 ]: 771 ax.set_ylabel(r"Photon flux / $\mathrm{s^{-1} \cdot cm^{-2}}$") 772 elif qty in [ 773 "curr_den_ib", 774 "curr_den_vb", 775 "curr_den_cb", 776 ]: 777 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 778 else: 779 fig = ax.get_figure() 780 781 qty_value = getattr(self.db_model_mpp, qty) 782 ax.axhline(qty_value, linestyle=":", color=color, label=label) 783 return fig 784 785 def plot_db_model_qty_vs_voltage( 786 self, 787 qty: str, 788 *, 789 ax: plt.Axes | None = None, 790 color: str | None = None, 791 label: str | None = None, 792 ) -> plt.Figure: 793 r"""Plot detailed balance model quantity vs voltage. 794 795 Args: 796 qty: Quantity, among 'potential_ii', 'potential_vi', 'potential_ic', 797 'potential_vc', 'phot_flux_ii', 'phot_flux_vi', 'phot_flux_ic', 798 'phot_flux_vc', 'curr_den_ib', 'curr_den_vb', and 'curr_den_cb'. 799 ax: Plot axes. If None, new figure and axes are created. 800 801 Returns: 802 Plot figure. 803 """ 804 if qty not in [ 805 "potential_ii", 806 "potential_vi", 807 "potential_ic", 808 "potential_vc", 809 "phot_flux_ii", 810 "phot_flux_vi", 811 "phot_flux_ic", 812 "phot_flux_vc", 813 "curr_den_ib", 814 "curr_den_vb", 815 "curr_den_cb", 816 ]: 817 raise ValueError("Quantity not supported!") 818 819 if ax is None: 820 fig, ax = plt.subplots(tight_layout=True) 821 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 822 if qty in [ 823 "potential_ii", 824 "potential_vi", 825 "potential_ic", 826 "potential_vc", 827 ]: 828 ax.set_ylabel(r"Chemical potential / $\mathrm{eV}$") 829 elif qty in [ 830 "phot_flux_ii", 831 "phot_flux_vi", 832 "phot_flux_ic", 833 "phot_flux_vc", 834 ]: 835 ax.set_ylabel(r"Photon flux / $\mathrm{s^{-1} \cdot cm^{-2}}$") 836 elif qty in [ 837 "curr_den_ib", 838 "curr_den_vb", 839 "curr_den_cb", 840 ]: 841 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 842 else: 843 fig = ax.get_figure() 844 845 qty_values = np.array( 846 [getattr(db_model, qty) for db_model in self.db_model_sweep], 847 dtype=float, 848 ) 849 ax.plot(self.voltage_sweep, qty_values, color=color, label=label) 850 return fig 851 852 @classmethod 853 def sq1961( 854 cls, 855 *, 856 bandgap_vc: Real, 857 bandwidth_vc: Real, 858 temperature: Real, 859 concentration: Real, 860 ) -> Self | None: 861 r"""Shockley-Queisser solar cell at given temperature and concentration. 862 863 Note: 864 See https://dx.doi.org/10.1063%2F1.1736034 for details. 865 866 Args: 867 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 868 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 869 temperature: Temperature [$\mathrm{K}$]. 870 concentration: Sunlight concentration factor. 871 872 Returns: 873 Applied solar cell. 874 """ 875 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.sq1961( 876 bandgap_vc=bandgap_vc, 877 bandwidth_vc=bandwidth_vc, 878 ) 879 if rsv_sp_absorb_ppoly is None: 880 return None 881 else: 882 return cls( 883 rsv_sp_absorb_ppoly, 884 temperature=temperature, 885 concentration=concentration, 886 ) 887 888 @classmethod 889 def lh2008( 890 cls, 891 *, 892 bandgap_vi: Real, 893 bandgap_ic: Real, 894 bandgap_vc: Real, 895 bandwidth_ii: Real, 896 bandwidth_vi: Real, 897 bandwidth_ic: Real, 898 bandwidth_vc: Real, 899 variant: str, 900 temperature: Real, 901 concentration: Real, 902 ) -> Self | None: 903 r"""Levi-Honsberg solar cell at given temperature and concentration. 904 905 Note: 906 See http://dx.doi.org/10.1103%2FPhysRevB.78.165122 for details. 907 908 Args: 909 bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$]. 910 bandgap_ic: Intermediate-conduction optical band gap 911 [$\mathrm{eV}$]. 912 bandgap_vc: Valence-conduction optical band gap 913 [$\mathrm{eV}$]. 914 bandwidth_ii: Intermediate-intermediate optical band width 915 [$\mathrm{eV}$]. 916 bandwidth_vi: Valence-intermediate optical band width 917 [$\mathrm{eV}$]. 918 bandwidth_ic: Intermediate-conduction optical band width 919 [$\mathrm{eV}$]. 920 bandwidth_vc: Valence-conduction optical band width 921 [$\mathrm{eV}$]. 922 variant: Model variant ('equal', 'inter', or 'intra'). 923 temperature: Temperature [$\mathrm{K}$]. 924 concentration: Sunlight concentration factor. 925 926 Returns: 927 Applied solar cell. 928 """ 929 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.lh2008( 930 bandgap_vi=bandgap_vi, 931 bandgap_ic=bandgap_ic, 932 bandgap_vc=bandgap_vc, 933 bandwidth_ii=bandwidth_ii, 934 bandwidth_vi=bandwidth_vi, 935 bandwidth_ic=bandwidth_ic, 936 bandwidth_vc=bandwidth_vc, 937 variant=variant, 938 ) 939 if rsv_sp_absorb_ppoly is None: 940 return None 941 else: 942 return cls( 943 rsv_sp_absorb_ppoly, 944 temperature=temperature, 945 concentration=concentration, 946 ) 947 948 @classmethod 949 def from_data( 950 cls, 951 energy: Sequence[Real, ...], 952 rsv_sp_absorb: Mapping[str, Sequence[Real, ...]], 953 *, 954 temperature: Real, 955 concentration: Real, 956 ) -> Self | None: 957 r"""Solar cell at given temperature and concentration from data. 958 959 Args: 960 energy: Photon energy values [$\mathrm{eV}$]. 961 rsv_sp_absorb: Mapping of transition labels ('ii', 'vi', 'ic', 'vc') 962 into corresponding spectral absorbance values. 963 temperature: Temperature [$\mathrm{K}$]. 964 concentration: Sunlight concentration factor. 965 966 Returns: 967 Applied solar cell. 968 """ 969 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.from_data( 970 energy, 971 rsv_sp_absorb, 972 ) 973 if rsv_sp_absorb_ppoly is None: 974 return None 975 else: 976 return cls( 977 rsv_sp_absorb_ppoly, 978 temperature=temperature, 979 concentration=concentration, 980 )
25class AppliedSolarCell: 26 r"""Solar cell at given temperature and Sunlight concentration factor. 27 28 Args: 29 rsv_sp_absorb_ppoly: Resolved spectral absorbance piecewise polynomials. 30 temperature: Temperature [$\mathrm{K}$]. 31 concentration: Sunlight concentration factor. 32 """ 33 34 def __init__( 35 self, 36 rsv_sp_absorb_ppoly: ResolvedSpectralAbsorbancePPoly, 37 *, 38 temperature: Real, 39 concentration: Real, 40 ) -> None: 41 r"""Initialize AppliedSolarCell object.""" 42 self.rsv_sp_absorb_ppoly = rsv_sp_absorb_ppoly 43 self.temperature = temperature 44 self.concentration = concentration 45 self.clear_voltage_sweep() 46 47 def __repr__(self) -> str: 48 r"""Represent AppliedSolarCell object.""" 49 return "\n".join( 50 [ 51 "Applied solar cell:", 52 f"- ii band / eV: [{self.onset_ii:.2f}, {self.offset_ii:.2f}]", 53 f"- vi band / eV: [{self.onset_vi:.2f}, {self.offset_vi:.2f}]", 54 f"- ic band / eV: [{self.onset_ic:.2f}, {self.offset_ic:.2f}]", 55 f"- vc band / eV: [{self.onset_vc:.2f}, {self.offset_vc:.2f}]", 56 f"- temperature / K: {self.temperature:.1f}", 57 f"- concentration: {self.concentration:.0f}", 58 f"- voltage sweep / V: {repr_num_seq(self.voltage_sweep, margin=22)}", 59 ] 60 ) 61 62 @property 63 def rsv_sp_absorb_ppoly(self) -> ResolvedSpectralAbsorbancePPoly: 64 r"""Resolved spectral absorbance piecewise polynomials.""" 65 return self._rsv_sp_absorb_ppoly 66 67 @rsv_sp_absorb_ppoly.setter 68 def rsv_sp_absorb_ppoly(self, arg: ResolvedSpectralAbsorbancePPoly) -> None: 69 if not isinstance(arg, ResolvedSpectralAbsorbancePPoly): 70 raise TypeError("Not a ResolvedSpectralAbsorbancePPoly object!") 71 self._rsv_sp_absorb_ppoly = arg 72 73 @property 74 def temperature(self) -> float: 75 r"""Temperature [$\mathrm{K}$].""" 76 return self._temperature 77 78 @temperature.setter 79 def temperature(self, arg: Real) -> None: 80 if not isinstance(arg, Real) or not arg > 0.0: 81 raise ValueError("Temperature must be a positive number!") 82 self._temperature = float(arg) 83 self.clear_cached_properties() 84 85 @property 86 def concentration(self) -> float: 87 r"""Sunlight concentration factor.""" 88 return self._concentration 89 90 @concentration.setter 91 def concentration(self, arg: Real) -> None: 92 if not isinstance(arg, Real) or not (0.0 <= arg <= 46200.0): 93 raise ValueError( 94 "Sunlight concentration factor must be between 0 and 46200!" 95 ) 96 self._concentration = float(arg) 97 self.clear_cached_properties() 98 99 @property 100 def onset_ii(self) -> float: 101 r"""Intermediate-intermediate onset [$\mathrm{eV}$].""" 102 if self._onset_ii is None: 103 sp_absorb_ii = self.rsv_sp_absorb_ppoly.ii 104 if sp_absorb_ii is not None: 105 self._onset_ii = sp_absorb_ii.onset 106 else: 107 self._onset_ii = np.nan 108 return self._onset_ii 109 110 @property 111 def offset_ii(self) -> float: 112 r"""Intermediate-intermediate offset [$\mathrm{eV}$].""" 113 if self._offset_ii is None: 114 sp_absorb_ii = self.rsv_sp_absorb_ppoly.ii 115 if sp_absorb_ii is not None: 116 self._offset_ii = sp_absorb_ii.offset 117 else: 118 self._offset_ii = np.nan 119 return self._offset_ii 120 121 @property 122 def onset_vi(self) -> float: 123 r"""Valence-intermediate onset [$\mathrm{eV}$].""" 124 if self._onset_vi is None: 125 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 126 if sp_absorb_vi is not None: 127 self._onset_vi = sp_absorb_vi.onset 128 else: 129 self._onset_vi = np.nan 130 return self._onset_vi 131 132 @property 133 def offset_vi(self) -> float: 134 r"""Valence-intermediate offset [$\mathrm{eV}$].""" 135 if self._offset_vi is None: 136 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 137 if sp_absorb_vi is not None: 138 self._offset_vi = sp_absorb_vi.offset 139 else: 140 self._offset_vi = np.nan 141 return self._offset_vi 142 143 @property 144 def onset_ic(self) -> float: 145 r"""Intermediate-conduction onset [$\mathrm{eV}$].""" 146 if self._onset_ic is None: 147 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 148 if sp_absorb_ic is not None: 149 self._onset_ic = sp_absorb_ic.onset 150 else: 151 self._onset_ic = np.nan 152 return self._onset_ic 153 154 @property 155 def offset_ic(self) -> float: 156 r"""Intermediate-conduction offset [$\mathrm{eV}$].""" 157 if self._offset_ic is None: 158 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 159 if sp_absorb_ic is not None: 160 self._offset_ic = sp_absorb_ic.offset 161 else: 162 self._offset_ic = np.nan 163 return self._offset_ic 164 165 @property 166 def onset_vc(self) -> float: 167 r"""Valence-conduction onset [$\mathrm{eV}$].""" 168 if self._onset_vc is None: 169 sp_absorb_vc = self.rsv_sp_absorb_ppoly.vc 170 if sp_absorb_vc is not None: 171 self._onset_vc = sp_absorb_vc.onset 172 else: 173 self._onset_vc = np.nan 174 return self._onset_vc 175 176 @property 177 def offset_vc(self) -> float: 178 r"""Valence-conduction offset [$\mathrm{eV}$].""" 179 if self._offset_vc is None: 180 sp_absorb_vc = self.rsv_sp_absorb_ppoly.vc 181 if sp_absorb_vc is not None: 182 self._offset_vc = sp_absorb_vc.offset 183 else: 184 self._offset_vc = np.nan 185 return self._offset_vc 186 187 @property 188 def voltage_ub(self) -> float: 189 r"""Voltage upper bound [$\mathrm{V}$].""" 190 if self._voltage_ub is None: 191 voltage_ub = min( 192 np.nan_to_num(self.onset_vi + self.onset_ic, nan=np.inf), 193 np.nan_to_num(self.onset_vc, nan=np.inf), 194 np.inf 195 ) 196 self._voltage_ub = float(voltage_ub) 197 return self._voltage_ub 198 199 @property 200 def voltage_oc(self) -> float: 201 r"""Open circuit voltage [$\mathrm{V}$].""" 202 if self._voltage_oc is None: 203 voltage_b = self.voltage_ub 204 voltage_a = voltage_b - 1.0 205 curr_den_a = self.curr_den_vs_voltage(voltage_a) 206 while curr_den_a > 0.0: 207 voltage_a -= 0.1 208 209 def objective(voltage): 210 if voltage == voltage_b: 211 return np.inf 212 else: 213 return self.curr_den_vs_voltage(voltage) 214 215 sol = root_scalar( 216 objective, 217 method="brentq", 218 bracket=[voltage_a, voltage_b], 219 ) 220 if not sol.converged: 221 print("Open circuit voltage did not converge!") 222 self._voltage_oc = np.nan 223 else: 224 self._voltage_oc = sol.root 225 226 return self._voltage_oc 227 228 @property 229 def curr_den_sc(self) -> float: 230 r"""Short circuit current density [$\mathrm{mA \, cm^{-2}}$].""" 231 if self._curr_den_sc is None: 232 self._curr_den_sc = self.curr_den_vs_voltage(0.0) 233 return self._curr_den_sc 234 235 @property 236 def voltage_mpp(self) -> float: 237 r"""Maximum power point voltage [$\mathrm{V}$].""" 238 if self._voltage_mpp is None: 239 res = minimize_scalar( 240 self.power_den_vs_voltage, 241 method="bounded", 242 bounds=sorted([0.0, self.voltage_oc]), 243 ) 244 if not res.success: 245 print("Maximum power point voltage did not converge!") 246 self._voltage_mpp = np.nan 247 else: 248 self._voltage_mpp = res.x 249 250 return self._voltage_mpp 251 252 @property 253 def db_model_mpp(self) -> DetailedBalanceModel: 254 r"""Maximum power point detailed balance model.""" 255 if self._db_model_mpp is None: 256 self._db_model_mpp = self.db_model_vs_voltage(self.voltage_mpp) 257 return self._db_model_mpp 258 259 @property 260 def curr_den_mpp(self) -> float: 261 r"""Maximum power point current density [$\mathrm{mA \, cm^{-2}}$].""" 262 if self._curr_den_mpp is None: 263 if abs(self.db_model_mpp.curr_den_vb) < abs( 264 self.db_model_mpp.curr_den_cb 265 ): 266 self._curr_den_mpp = self.db_model_mpp.curr_den_vb 267 else: 268 self._curr_den_mpp = self.db_model_mpp.curr_den_cb 269 return self._curr_den_mpp 270 271 @property 272 def power_den_mpp(self) -> float: 273 r"""Maximum power point power density [$\mathrm{mW \, cm^{-2}}$].""" 274 if self._power_den_mpp is None: 275 self._power_den_mpp = self.curr_den_mpp * self.voltage_mpp 276 return self._power_den_mpp 277 278 @property 279 def efficiency_mpp(self) -> float: 280 r"""Maximum power point power conversion efficiency [$\mathrm{\%}$].""" 281 if self._efficiency_mpp is None: 282 self._efficiency_mpp = ( 283 -100.0 284 * self.power_den_mpp 285 / (self.concentration * Constant.psun) 286 ) 287 return self._efficiency_mpp 288 289 @property 290 def fill_factor(self) -> float: 291 r"""Fill factor.""" 292 if self._fill_factor is None: 293 self._fill_factor = self.power_den_mpp / ( 294 self.voltage_oc * self.curr_den_sc 295 ) 296 return self._fill_factor 297 298 @property 299 def voltage_sweep(self) -> np.ndarray[float]: 300 r"""Voltage sweep [$\mathrm{V}$].""" 301 return self._voltage_sweep 302 303 @property 304 def db_model_sweep(self) -> list[DetailedBalanceModel, ...]: 305 r"""Detailed balance model sweep.""" 306 if self._db_model_sweep is None: 307 self._db_model_sweep = [ 308 self.db_model_vs_voltage(voltage) 309 for voltage in self.voltage_sweep 310 ] 311 return self._db_model_sweep 312 313 @property 314 def curr_den_sweep(self) -> np.ndarray[float]: 315 r"""Current density sweep [$\mathrm{mA \, cm^{-2}}$].""" 316 if self._curr_den_sweep is None: 317 self._curr_den_sweep = np.array([], dtype=float) 318 for db_model in self.db_model_sweep: 319 if abs(db_model.curr_den_vb) < abs(db_model.curr_den_cb): 320 self._curr_den_sweep = np.append( 321 self._curr_den_sweep, 322 db_model.curr_den_vb, 323 ) 324 else: 325 self._curr_den_sweep = np.append( 326 self._curr_den_sweep, 327 db_model.curr_den_cb, 328 ) 329 return self._curr_den_sweep 330 331 @property 332 def power_den_sweep(self) -> np.ndarray[float]: 333 r"""Power density sweep [$\mathrm{mW \, cm^{-2}}$].""" 334 if self._power_den_sweep is None: 335 self._power_den_sweep = self.curr_den_sweep * self.voltage_sweep 336 return self._power_den_sweep 337 338 @property 339 def efficiency_sweep(self) -> np.ndarray[float]: 340 r"""Power conversion efficiency sweep [$\mathrm{\%}$].""" 341 if self._efficiency_sweep is None: 342 self._efficiency_sweep = ( 343 -100.0 344 * self.power_den_sweep 345 / (self.concentration * Constant.psun) 346 ) 347 return self._efficiency_sweep 348 349 def clear_cached_properties(self) -> None: 350 r"""Clear cached properties.""" 351 self._onset_ii = None 352 self._offset_ii = None 353 self._onset_vi = None 354 self._offset_vi = None 355 self._onset_ic = None 356 self._offset_ic = None 357 self._onset_vc = None 358 self._offset_vc = None 359 self._voltage_ub = None 360 self._voltage_oc = None 361 self._curr_den_sc = None 362 self._voltage_mpp = None 363 self._db_model_mpp = None 364 self._curr_den_mpp = None 365 self._power_den_mpp = None 366 self._efficiency_mpp = None 367 self._fill_factor = None 368 self._db_model_sweep = None 369 self._curr_den_sweep = None 370 self._power_den_sweep = None 371 self._efficiency_sweep = None 372 373 def db_model_vs_voltage(self, voltage: Real) -> DetailedBalanceModel: 374 r"""Detailed balance model vs voltage. 375 376 Args: 377 voltage: Voltage [$\mathrm{V}$]. 378 379 Returns: 380 Detailed balance model. 381 """ 382 return DetailedBalanceModel( 383 self.rsv_sp_absorb_ppoly, 384 temperature=self.temperature, 385 voltage=voltage, 386 concentration=self.concentration, 387 ) 388 389 def curr_den_vs_voltage(self, voltage: Real) -> float: 390 r"""Current density vs voltage. 391 392 Note: 393 Valence and conduction band current densities should be the same. 394 However, the condition of zero intermediate band current density 395 cannot always be reached, such as in narrow-gap intermediate 396 band solar cells at high concentration. 397 This introduces a non-negligible difference between valence and 398 conduction band current densities, according to the continuity 399 equation. 400 Currently, the smaller among the two current densities is taken 401 as the solar cell current density. 402 This is particularly helpful to aid numerical optimization 403 methods. 404 405 Args: 406 voltage: Voltage [$\mathrm{V}$]. 407 408 Returns: 409 Current density [$\mathrm{mA \, cm^{-2}}$]. 410 """ 411 db_model = self.db_model_vs_voltage(voltage) 412 if abs(db_model.curr_den_vb) < abs(db_model.curr_den_cb): 413 return db_model.curr_den_vb 414 else: 415 return db_model.curr_den_cb 416 417 def power_den_vs_voltage(self, voltage: Real) -> float: 418 r"""Power density vs voltage. 419 420 Args: 421 voltage: Voltage [$\mathrm{V}$]. 422 423 Returns: 424 Power density [$\mathrm{mW \, cm^{-2}}$]. 425 """ 426 return voltage * self.curr_den_vs_voltage(voltage) 427 428 def apply_voltage_sweep(self, voltage_sweep: Sequence[Real, ...]) -> None: 429 r"""Apply voltage sweep.""" 430 self._voltage_sweep = np.unique(voltage_sweep).astype(float) 431 self._db_model_sweep = None 432 self._curr_den_sweep = None 433 self._power_den_sweep = None 434 self._efficiency_sweep = None 435 436 def clear_voltage_sweep(self) -> None: 437 r"""Clear voltage sweep.""" 438 self.apply_voltage_sweep([]) 439 440 def plot_rsv_sp_absorb_vs_energy( 441 self, 442 *, 443 energy_min: Real, 444 energy_max: Real, 445 energy_inc: Real, 446 ax: plt.Axes | None = None, 447 ) -> plt.Figure | None: 448 r"""Plot resolved spectral absorbance vs photon energy. 449 450 Args: 451 energy_min: Photon energy minimum [$\mathrm{eV}$]. 452 energy_max: Photon energy maximum [$\mathrm{eV}$]. 453 energy_inc: Photon energy increment [$\mathrm{eV}$]. 454 ax: Plot axes. If None, new figure and axes are created. 455 456 Returns: 457 Plot figure. 458 """ 459 return self.rsv_sp_absorb_ppoly.plot_rsv_sp_absorb_vs_energy( 460 energy_min=energy_min, 461 energy_max=energy_max, 462 energy_inc=energy_inc, 463 ax=ax, 464 ) 465 466 def plot_voltage_oc( 467 self, 468 *, 469 ax: plt.Axes | None = None, 470 color: str | None = "k", 471 label: str | None = None, 472 ) -> plt.Figure: 473 r"""Plot open circuit voltage. 474 475 Args: 476 ax: Plot axes. If None, new figure and axes are created. 477 color: Color. 478 label: Label. 479 480 Returns: 481 Plot figure. 482 """ 483 if ax is None: 484 fig, ax = plt.subplots(tight_layout=True) 485 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 486 else: 487 fig = ax.get_figure() 488 489 ax.axvline(self.voltage_oc, linestyle=":", color=color, label=label) 490 return fig 491 492 def plot_curr_den_sc( 493 self, 494 *, 495 ax: plt.Axes | None = None, 496 color: str | None = "k", 497 label: str | None = None, 498 ) -> plt.Figure: 499 r"""Plot short circuit current density. 500 501 Args: 502 ax: Plot axes. If None, new figure and axes are created. 503 color: Color. 504 label: Label. 505 506 Returns: 507 Plot figure. 508 """ 509 if ax is None: 510 fig, ax = plt.subplots(tight_layout=True) 511 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 512 else: 513 fig = ax.get_figure() 514 515 ax.axhline(self.curr_den_sc, linestyle=":", color=color, label=label) 516 return fig 517 518 def plot_voltage_mpp( 519 self, 520 *, 521 ax: plt.Axes | None = None, 522 color: str | None = "k", 523 label: str | None = None, 524 ) -> plt.Figure: 525 r"""Plot maximum power point voltage. 526 527 Args: 528 ax: Plot axes. If None, new figure and axes are created. 529 color: Color. 530 label: Label. 531 532 Returns: 533 Plot figure. 534 """ 535 if ax is None: 536 fig, ax = plt.subplots(tight_layout=True) 537 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 538 else: 539 fig = ax.get_figure() 540 541 ax.axvline(self.voltage_mpp, linestyle=":", color=color, label=label) 542 return fig 543 544 def plot_curr_den_mpp( 545 self, 546 *, 547 ax: plt.Axes | None = None, 548 color: str | None = "k", 549 label: str | None = None, 550 ) -> plt.Figure: 551 r"""Plot maximum power point current density. 552 553 Args: 554 ax: Plot axes. If None, new figure and axes are created. 555 color: Color. 556 label: Label. 557 558 Returns: 559 Plot figure. 560 """ 561 if ax is None: 562 fig, ax = plt.subplots(tight_layout=True) 563 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 564 else: 565 fig = ax.get_figure() 566 567 ax.axhline(self.curr_den_mpp, linestyle=":", color=color, label=label) 568 return fig 569 570 def plot_power_den_mpp( 571 self, 572 *, 573 ax: plt.Axes | None = None, 574 color: str | None = "k", 575 label: str | None = None, 576 ) -> plt.Figure: 577 r"""Plot maximum power point power density. 578 579 Args: 580 ax: Plot axes. If None, new figure and axes are created. 581 color: Color. 582 label: Label. 583 584 Returns: 585 Plot figure. 586 """ 587 if ax is None: 588 fig, ax = plt.subplots(tight_layout=True) 589 ax.set_ylabel(r"Power density / $\mathrm{mA \cdot cm^{-2}}$") 590 else: 591 fig = ax.get_figure() 592 593 ax.axhline(self.power_den_mpp, linestyle=":", color=color, label=label) 594 return fig 595 596 def plot_efficiency_mpp( 597 self, 598 *, 599 ax: plt.Axes | None = None, 600 color: str | None = "k", 601 label: str | None = None, 602 ) -> plt.Figure: 603 r"""Plot maximum power point efficiency. 604 605 Args: 606 ax: Plot axes. If None, new figure and axes are created. 607 color: Color. 608 label: Label. 609 610 Returns: 611 Plot figure. 612 """ 613 if ax is None: 614 fig, ax = plt.subplots(tight_layout=True) 615 ax.set_ylabel(r"Efficiency / $\mathrm{\%}$") 616 else: 617 fig = ax.get_figure() 618 619 ax.axhline(self.efficiency_mpp, linestyle=":", color=color, label=label) 620 return fig 621 622 def plot_curr_den_vs_voltage( 623 self, 624 *, 625 ax: plt.Axes | None = None, 626 color: str | None = None, 627 label: str | None = None, 628 ) -> plt.Figure: 629 r"""Plot current density vs voltage. 630 631 Args: 632 color: Color. 633 label: Label. 634 ax: Plot axes. If None, new figure and axes are created. 635 636 Returns: 637 Plot figure. 638 """ 639 if ax is None: 640 fig, ax = plt.subplots(tight_layout=True) 641 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 642 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 643 ax.set_ylim(ceil(self.curr_den_sc / 10) * 10 - 20, 0.0) 644 else: 645 fig = ax.get_figure() 646 647 ax.plot( 648 self.voltage_sweep, 649 self.curr_den_sweep, 650 color=color, 651 label=label, 652 ) 653 return fig 654 655 def plot_power_den_vs_voltage( 656 self, 657 *, 658 ax: plt.Axes | None = None, 659 color: str | None = None, 660 label: str | None = None, 661 ) -> plt.Figure: 662 r"""Plot power density vs voltage. 663 664 Args: 665 ax: Plot axes. If None, new figure and axes are created. 666 color: Color. 667 label: Label. 668 669 Returns: 670 Plot figure. 671 """ 672 if ax is None: 673 fig, ax = plt.subplots(tight_layout=True) 674 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 675 ax.set_ylabel(r"Power density / $\mathrm{mW \cdot cm^{-2}}$") 676 ax.set_ylim(ceil(self.power_den_mpp / 10) * 10 - 20, 0.0) 677 else: 678 fig = ax.get_figure() 679 680 ax.plot( 681 self.voltage_sweep, 682 self.power_den_sweep, 683 color=color, 684 label=label, 685 ) 686 return fig 687 688 def plot_efficiency_vs_voltage( 689 self, 690 *, 691 ax: plt.Axes | None = None, 692 color: str | None = None, 693 label: str | None = None, 694 ) -> plt.Figure: 695 r"""Plot efficiency vs voltage. 696 697 Args: 698 ax: Plot axes. If None, new figure and axes are created. 699 color: Color. 700 label: Label. 701 702 Returns: 703 Plot figure. 704 """ 705 if ax is None: 706 fig, ax = plt.subplots(tight_layout=True) 707 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 708 ax.set_ylabel(r"Efficiency / $\mathrm{\%}$") 709 ax.set_ylim(0.0, ceil(self.efficiency_mpp / 10) * 10 + 10) 710 else: 711 fig = ax.get_figure() 712 713 ax.plot( 714 self.voltage_sweep, 715 self.efficiency_sweep, 716 color=color, 717 label=label, 718 ) 719 return fig 720 721 def plot_db_model_qty_mpp( 722 self, 723 qty: str, 724 *, 725 ax: plt.Axes | None = None, 726 color: str | None = "k", 727 label: str | None = None, 728 ) -> plt.Figure: 729 r"""Plot maximum power point detailed balance model quantity. 730 731 Args: 732 qty: Quantity, among 'potential_ii', 'potential_vi', 'potential_ic', 733 'potential_vc', 'phot_flux_ii', 'phot_flux_vi', 'phot_flux_ic', 734 'phot_flux_vc', 'curr_den_ib', 'curr_den_vb', and 'curr_den_cb'. 735 ax: Plot axes. If None, new figure and axes are created. 736 color: Color. 737 label: Label. 738 739 Returns: 740 Plot figure. 741 """ 742 if qty not in [ 743 "potential_ii", 744 "potential_vi", 745 "potential_ic", 746 "potential_vc", 747 "phot_flux_ii", 748 "phot_flux_vi", 749 "phot_flux_ic", 750 "phot_flux_vc", 751 "curr_den_ib", 752 "curr_den_vb", 753 "curr_den_cb", 754 ]: 755 raise ValueError("Quantity not supported!") 756 757 if ax is None: 758 fig, ax = plt.subplots(tight_layout=True) 759 if qty in [ 760 "potential_ii", 761 "potential_vi", 762 "potential_ic", 763 "potential_vc", 764 ]: 765 ax.set_ylabel(r"Chemical potential / $\mathrm{eV}$") 766 elif qty in [ 767 "phot_flux_ii", 768 "phot_flux_vi", 769 "phot_flux_ic", 770 "phot_flux_vc", 771 ]: 772 ax.set_ylabel(r"Photon flux / $\mathrm{s^{-1} \cdot cm^{-2}}$") 773 elif qty in [ 774 "curr_den_ib", 775 "curr_den_vb", 776 "curr_den_cb", 777 ]: 778 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 779 else: 780 fig = ax.get_figure() 781 782 qty_value = getattr(self.db_model_mpp, qty) 783 ax.axhline(qty_value, linestyle=":", color=color, label=label) 784 return fig 785 786 def plot_db_model_qty_vs_voltage( 787 self, 788 qty: str, 789 *, 790 ax: plt.Axes | None = None, 791 color: str | None = None, 792 label: str | None = None, 793 ) -> plt.Figure: 794 r"""Plot detailed balance model quantity vs voltage. 795 796 Args: 797 qty: Quantity, among 'potential_ii', 'potential_vi', 'potential_ic', 798 'potential_vc', 'phot_flux_ii', 'phot_flux_vi', 'phot_flux_ic', 799 'phot_flux_vc', 'curr_den_ib', 'curr_den_vb', and 'curr_den_cb'. 800 ax: Plot axes. If None, new figure and axes are created. 801 802 Returns: 803 Plot figure. 804 """ 805 if qty not in [ 806 "potential_ii", 807 "potential_vi", 808 "potential_ic", 809 "potential_vc", 810 "phot_flux_ii", 811 "phot_flux_vi", 812 "phot_flux_ic", 813 "phot_flux_vc", 814 "curr_den_ib", 815 "curr_den_vb", 816 "curr_den_cb", 817 ]: 818 raise ValueError("Quantity not supported!") 819 820 if ax is None: 821 fig, ax = plt.subplots(tight_layout=True) 822 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 823 if qty in [ 824 "potential_ii", 825 "potential_vi", 826 "potential_ic", 827 "potential_vc", 828 ]: 829 ax.set_ylabel(r"Chemical potential / $\mathrm{eV}$") 830 elif qty in [ 831 "phot_flux_ii", 832 "phot_flux_vi", 833 "phot_flux_ic", 834 "phot_flux_vc", 835 ]: 836 ax.set_ylabel(r"Photon flux / $\mathrm{s^{-1} \cdot cm^{-2}}$") 837 elif qty in [ 838 "curr_den_ib", 839 "curr_den_vb", 840 "curr_den_cb", 841 ]: 842 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 843 else: 844 fig = ax.get_figure() 845 846 qty_values = np.array( 847 [getattr(db_model, qty) for db_model in self.db_model_sweep], 848 dtype=float, 849 ) 850 ax.plot(self.voltage_sweep, qty_values, color=color, label=label) 851 return fig 852 853 @classmethod 854 def sq1961( 855 cls, 856 *, 857 bandgap_vc: Real, 858 bandwidth_vc: Real, 859 temperature: Real, 860 concentration: Real, 861 ) -> Self | None: 862 r"""Shockley-Queisser solar cell at given temperature and concentration. 863 864 Note: 865 See https://dx.doi.org/10.1063%2F1.1736034 for details. 866 867 Args: 868 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 869 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 870 temperature: Temperature [$\mathrm{K}$]. 871 concentration: Sunlight concentration factor. 872 873 Returns: 874 Applied solar cell. 875 """ 876 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.sq1961( 877 bandgap_vc=bandgap_vc, 878 bandwidth_vc=bandwidth_vc, 879 ) 880 if rsv_sp_absorb_ppoly is None: 881 return None 882 else: 883 return cls( 884 rsv_sp_absorb_ppoly, 885 temperature=temperature, 886 concentration=concentration, 887 ) 888 889 @classmethod 890 def lh2008( 891 cls, 892 *, 893 bandgap_vi: Real, 894 bandgap_ic: Real, 895 bandgap_vc: Real, 896 bandwidth_ii: Real, 897 bandwidth_vi: Real, 898 bandwidth_ic: Real, 899 bandwidth_vc: Real, 900 variant: str, 901 temperature: Real, 902 concentration: Real, 903 ) -> Self | None: 904 r"""Levi-Honsberg solar cell at given temperature and concentration. 905 906 Note: 907 See http://dx.doi.org/10.1103%2FPhysRevB.78.165122 for details. 908 909 Args: 910 bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$]. 911 bandgap_ic: Intermediate-conduction optical band gap 912 [$\mathrm{eV}$]. 913 bandgap_vc: Valence-conduction optical band gap 914 [$\mathrm{eV}$]. 915 bandwidth_ii: Intermediate-intermediate optical band width 916 [$\mathrm{eV}$]. 917 bandwidth_vi: Valence-intermediate optical band width 918 [$\mathrm{eV}$]. 919 bandwidth_ic: Intermediate-conduction optical band width 920 [$\mathrm{eV}$]. 921 bandwidth_vc: Valence-conduction optical band width 922 [$\mathrm{eV}$]. 923 variant: Model variant ('equal', 'inter', or 'intra'). 924 temperature: Temperature [$\mathrm{K}$]. 925 concentration: Sunlight concentration factor. 926 927 Returns: 928 Applied solar cell. 929 """ 930 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.lh2008( 931 bandgap_vi=bandgap_vi, 932 bandgap_ic=bandgap_ic, 933 bandgap_vc=bandgap_vc, 934 bandwidth_ii=bandwidth_ii, 935 bandwidth_vi=bandwidth_vi, 936 bandwidth_ic=bandwidth_ic, 937 bandwidth_vc=bandwidth_vc, 938 variant=variant, 939 ) 940 if rsv_sp_absorb_ppoly is None: 941 return None 942 else: 943 return cls( 944 rsv_sp_absorb_ppoly, 945 temperature=temperature, 946 concentration=concentration, 947 ) 948 949 @classmethod 950 def from_data( 951 cls, 952 energy: Sequence[Real, ...], 953 rsv_sp_absorb: Mapping[str, Sequence[Real, ...]], 954 *, 955 temperature: Real, 956 concentration: Real, 957 ) -> Self | None: 958 r"""Solar cell at given temperature and concentration from data. 959 960 Args: 961 energy: Photon energy values [$\mathrm{eV}$]. 962 rsv_sp_absorb: Mapping of transition labels ('ii', 'vi', 'ic', 'vc') 963 into corresponding spectral absorbance values. 964 temperature: Temperature [$\mathrm{K}$]. 965 concentration: Sunlight concentration factor. 966 967 Returns: 968 Applied solar cell. 969 """ 970 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.from_data( 971 energy, 972 rsv_sp_absorb, 973 ) 974 if rsv_sp_absorb_ppoly is None: 975 return None 976 else: 977 return cls( 978 rsv_sp_absorb_ppoly, 979 temperature=temperature, 980 concentration=concentration, 981 )
Solar cell at given temperature and Sunlight concentration factor.
Arguments:
- rsv_sp_absorb_ppoly: Resolved spectral absorbance piecewise polynomials.
- temperature: Temperature [$\mathrm{K}$].
- concentration: Sunlight concentration factor.
34 def __init__( 35 self, 36 rsv_sp_absorb_ppoly: ResolvedSpectralAbsorbancePPoly, 37 *, 38 temperature: Real, 39 concentration: Real, 40 ) -> None: 41 r"""Initialize AppliedSolarCell object.""" 42 self.rsv_sp_absorb_ppoly = rsv_sp_absorb_ppoly 43 self.temperature = temperature 44 self.concentration = concentration 45 self.clear_voltage_sweep()
Initialize AppliedSolarCell object.
62 @property 63 def rsv_sp_absorb_ppoly(self) -> ResolvedSpectralAbsorbancePPoly: 64 r"""Resolved spectral absorbance piecewise polynomials.""" 65 return self._rsv_sp_absorb_ppoly
Resolved spectral absorbance piecewise polynomials.
73 @property 74 def temperature(self) -> float: 75 r"""Temperature [$\mathrm{K}$].""" 76 return self._temperature
Temperature [$\mathrm{K}$].
85 @property 86 def concentration(self) -> float: 87 r"""Sunlight concentration factor.""" 88 return self._concentration
Sunlight concentration factor.
99 @property 100 def onset_ii(self) -> float: 101 r"""Intermediate-intermediate onset [$\mathrm{eV}$].""" 102 if self._onset_ii is None: 103 sp_absorb_ii = self.rsv_sp_absorb_ppoly.ii 104 if sp_absorb_ii is not None: 105 self._onset_ii = sp_absorb_ii.onset 106 else: 107 self._onset_ii = np.nan 108 return self._onset_ii
Intermediate-intermediate onset [$\mathrm{eV}$].
110 @property 111 def offset_ii(self) -> float: 112 r"""Intermediate-intermediate offset [$\mathrm{eV}$].""" 113 if self._offset_ii is None: 114 sp_absorb_ii = self.rsv_sp_absorb_ppoly.ii 115 if sp_absorb_ii is not None: 116 self._offset_ii = sp_absorb_ii.offset 117 else: 118 self._offset_ii = np.nan 119 return self._offset_ii
Intermediate-intermediate offset [$\mathrm{eV}$].
121 @property 122 def onset_vi(self) -> float: 123 r"""Valence-intermediate onset [$\mathrm{eV}$].""" 124 if self._onset_vi is None: 125 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 126 if sp_absorb_vi is not None: 127 self._onset_vi = sp_absorb_vi.onset 128 else: 129 self._onset_vi = np.nan 130 return self._onset_vi
Valence-intermediate onset [$\mathrm{eV}$].
132 @property 133 def offset_vi(self) -> float: 134 r"""Valence-intermediate offset [$\mathrm{eV}$].""" 135 if self._offset_vi is None: 136 sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi 137 if sp_absorb_vi is not None: 138 self._offset_vi = sp_absorb_vi.offset 139 else: 140 self._offset_vi = np.nan 141 return self._offset_vi
Valence-intermediate offset [$\mathrm{eV}$].
143 @property 144 def onset_ic(self) -> float: 145 r"""Intermediate-conduction onset [$\mathrm{eV}$].""" 146 if self._onset_ic is None: 147 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 148 if sp_absorb_ic is not None: 149 self._onset_ic = sp_absorb_ic.onset 150 else: 151 self._onset_ic = np.nan 152 return self._onset_ic
Intermediate-conduction onset [$\mathrm{eV}$].
154 @property 155 def offset_ic(self) -> float: 156 r"""Intermediate-conduction offset [$\mathrm{eV}$].""" 157 if self._offset_ic is None: 158 sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic 159 if sp_absorb_ic is not None: 160 self._offset_ic = sp_absorb_ic.offset 161 else: 162 self._offset_ic = np.nan 163 return self._offset_ic
Intermediate-conduction offset [$\mathrm{eV}$].
165 @property 166 def onset_vc(self) -> float: 167 r"""Valence-conduction onset [$\mathrm{eV}$].""" 168 if self._onset_vc is None: 169 sp_absorb_vc = self.rsv_sp_absorb_ppoly.vc 170 if sp_absorb_vc is not None: 171 self._onset_vc = sp_absorb_vc.onset 172 else: 173 self._onset_vc = np.nan 174 return self._onset_vc
Valence-conduction onset [$\mathrm{eV}$].
176 @property 177 def offset_vc(self) -> float: 178 r"""Valence-conduction offset [$\mathrm{eV}$].""" 179 if self._offset_vc is None: 180 sp_absorb_vc = self.rsv_sp_absorb_ppoly.vc 181 if sp_absorb_vc is not None: 182 self._offset_vc = sp_absorb_vc.offset 183 else: 184 self._offset_vc = np.nan 185 return self._offset_vc
Valence-conduction offset [$\mathrm{eV}$].
187 @property 188 def voltage_ub(self) -> float: 189 r"""Voltage upper bound [$\mathrm{V}$].""" 190 if self._voltage_ub is None: 191 voltage_ub = min( 192 np.nan_to_num(self.onset_vi + self.onset_ic, nan=np.inf), 193 np.nan_to_num(self.onset_vc, nan=np.inf), 194 np.inf 195 ) 196 self._voltage_ub = float(voltage_ub) 197 return self._voltage_ub
Voltage upper bound [$\mathrm{V}$].
199 @property 200 def voltage_oc(self) -> float: 201 r"""Open circuit voltage [$\mathrm{V}$].""" 202 if self._voltage_oc is None: 203 voltage_b = self.voltage_ub 204 voltage_a = voltage_b - 1.0 205 curr_den_a = self.curr_den_vs_voltage(voltage_a) 206 while curr_den_a > 0.0: 207 voltage_a -= 0.1 208 209 def objective(voltage): 210 if voltage == voltage_b: 211 return np.inf 212 else: 213 return self.curr_den_vs_voltage(voltage) 214 215 sol = root_scalar( 216 objective, 217 method="brentq", 218 bracket=[voltage_a, voltage_b], 219 ) 220 if not sol.converged: 221 print("Open circuit voltage did not converge!") 222 self._voltage_oc = np.nan 223 else: 224 self._voltage_oc = sol.root 225 226 return self._voltage_oc
Open circuit voltage [$\mathrm{V}$].
228 @property 229 def curr_den_sc(self) -> float: 230 r"""Short circuit current density [$\mathrm{mA \, cm^{-2}}$].""" 231 if self._curr_den_sc is None: 232 self._curr_den_sc = self.curr_den_vs_voltage(0.0) 233 return self._curr_den_sc
Short circuit current density [$\mathrm{mA \, cm^{-2}}$].
235 @property 236 def voltage_mpp(self) -> float: 237 r"""Maximum power point voltage [$\mathrm{V}$].""" 238 if self._voltage_mpp is None: 239 res = minimize_scalar( 240 self.power_den_vs_voltage, 241 method="bounded", 242 bounds=sorted([0.0, self.voltage_oc]), 243 ) 244 if not res.success: 245 print("Maximum power point voltage did not converge!") 246 self._voltage_mpp = np.nan 247 else: 248 self._voltage_mpp = res.x 249 250 return self._voltage_mpp
Maximum power point voltage [$\mathrm{V}$].
252 @property 253 def db_model_mpp(self) -> DetailedBalanceModel: 254 r"""Maximum power point detailed balance model.""" 255 if self._db_model_mpp is None: 256 self._db_model_mpp = self.db_model_vs_voltage(self.voltage_mpp) 257 return self._db_model_mpp
Maximum power point detailed balance model.
259 @property 260 def curr_den_mpp(self) -> float: 261 r"""Maximum power point current density [$\mathrm{mA \, cm^{-2}}$].""" 262 if self._curr_den_mpp is None: 263 if abs(self.db_model_mpp.curr_den_vb) < abs( 264 self.db_model_mpp.curr_den_cb 265 ): 266 self._curr_den_mpp = self.db_model_mpp.curr_den_vb 267 else: 268 self._curr_den_mpp = self.db_model_mpp.curr_den_cb 269 return self._curr_den_mpp
Maximum power point current density [$\mathrm{mA \, cm^{-2}}$].
271 @property 272 def power_den_mpp(self) -> float: 273 r"""Maximum power point power density [$\mathrm{mW \, cm^{-2}}$].""" 274 if self._power_den_mpp is None: 275 self._power_den_mpp = self.curr_den_mpp * self.voltage_mpp 276 return self._power_den_mpp
Maximum power point power density [$\mathrm{mW \, cm^{-2}}$].
278 @property 279 def efficiency_mpp(self) -> float: 280 r"""Maximum power point power conversion efficiency [$\mathrm{\%}$].""" 281 if self._efficiency_mpp is None: 282 self._efficiency_mpp = ( 283 -100.0 284 * self.power_den_mpp 285 / (self.concentration * Constant.psun) 286 ) 287 return self._efficiency_mpp
Maximum power point power conversion efficiency [$\mathrm{\%}$].
289 @property 290 def fill_factor(self) -> float: 291 r"""Fill factor.""" 292 if self._fill_factor is None: 293 self._fill_factor = self.power_den_mpp / ( 294 self.voltage_oc * self.curr_den_sc 295 ) 296 return self._fill_factor
Fill factor.
298 @property 299 def voltage_sweep(self) -> np.ndarray[float]: 300 r"""Voltage sweep [$\mathrm{V}$].""" 301 return self._voltage_sweep
Voltage sweep [$\mathrm{V}$].
303 @property 304 def db_model_sweep(self) -> list[DetailedBalanceModel, ...]: 305 r"""Detailed balance model sweep.""" 306 if self._db_model_sweep is None: 307 self._db_model_sweep = [ 308 self.db_model_vs_voltage(voltage) 309 for voltage in self.voltage_sweep 310 ] 311 return self._db_model_sweep
Detailed balance model sweep.
313 @property 314 def curr_den_sweep(self) -> np.ndarray[float]: 315 r"""Current density sweep [$\mathrm{mA \, cm^{-2}}$].""" 316 if self._curr_den_sweep is None: 317 self._curr_den_sweep = np.array([], dtype=float) 318 for db_model in self.db_model_sweep: 319 if abs(db_model.curr_den_vb) < abs(db_model.curr_den_cb): 320 self._curr_den_sweep = np.append( 321 self._curr_den_sweep, 322 db_model.curr_den_vb, 323 ) 324 else: 325 self._curr_den_sweep = np.append( 326 self._curr_den_sweep, 327 db_model.curr_den_cb, 328 ) 329 return self._curr_den_sweep
Current density sweep [$\mathrm{mA \, cm^{-2}}$].
331 @property 332 def power_den_sweep(self) -> np.ndarray[float]: 333 r"""Power density sweep [$\mathrm{mW \, cm^{-2}}$].""" 334 if self._power_den_sweep is None: 335 self._power_den_sweep = self.curr_den_sweep * self.voltage_sweep 336 return self._power_den_sweep
Power density sweep [$\mathrm{mW \, cm^{-2}}$].
338 @property 339 def efficiency_sweep(self) -> np.ndarray[float]: 340 r"""Power conversion efficiency sweep [$\mathrm{\%}$].""" 341 if self._efficiency_sweep is None: 342 self._efficiency_sweep = ( 343 -100.0 344 * self.power_den_sweep 345 / (self.concentration * Constant.psun) 346 ) 347 return self._efficiency_sweep
Power conversion efficiency sweep [$\mathrm{\%}$].
349 def clear_cached_properties(self) -> None: 350 r"""Clear cached properties.""" 351 self._onset_ii = None 352 self._offset_ii = None 353 self._onset_vi = None 354 self._offset_vi = None 355 self._onset_ic = None 356 self._offset_ic = None 357 self._onset_vc = None 358 self._offset_vc = None 359 self._voltage_ub = None 360 self._voltage_oc = None 361 self._curr_den_sc = None 362 self._voltage_mpp = None 363 self._db_model_mpp = None 364 self._curr_den_mpp = None 365 self._power_den_mpp = None 366 self._efficiency_mpp = None 367 self._fill_factor = None 368 self._db_model_sweep = None 369 self._curr_den_sweep = None 370 self._power_den_sweep = None 371 self._efficiency_sweep = None
Clear cached properties.
373 def db_model_vs_voltage(self, voltage: Real) -> DetailedBalanceModel: 374 r"""Detailed balance model vs voltage. 375 376 Args: 377 voltage: Voltage [$\mathrm{V}$]. 378 379 Returns: 380 Detailed balance model. 381 """ 382 return DetailedBalanceModel( 383 self.rsv_sp_absorb_ppoly, 384 temperature=self.temperature, 385 voltage=voltage, 386 concentration=self.concentration, 387 )
Detailed balance model vs voltage.
Arguments:
- voltage: Voltage [$\mathrm{V}$].
Returns:
Detailed balance model.
389 def curr_den_vs_voltage(self, voltage: Real) -> float: 390 r"""Current density vs voltage. 391 392 Note: 393 Valence and conduction band current densities should be the same. 394 However, the condition of zero intermediate band current density 395 cannot always be reached, such as in narrow-gap intermediate 396 band solar cells at high concentration. 397 This introduces a non-negligible difference between valence and 398 conduction band current densities, according to the continuity 399 equation. 400 Currently, the smaller among the two current densities is taken 401 as the solar cell current density. 402 This is particularly helpful to aid numerical optimization 403 methods. 404 405 Args: 406 voltage: Voltage [$\mathrm{V}$]. 407 408 Returns: 409 Current density [$\mathrm{mA \, cm^{-2}}$]. 410 """ 411 db_model = self.db_model_vs_voltage(voltage) 412 if abs(db_model.curr_den_vb) < abs(db_model.curr_den_cb): 413 return db_model.curr_den_vb 414 else: 415 return db_model.curr_den_cb
Current density vs voltage.
Note:
Valence and conduction band current densities should be the same. However, the condition of zero intermediate band current density cannot always be reached, such as in narrow-gap intermediate band solar cells at high concentration. This introduces a non-negligible difference between valence and conduction band current densities, according to the continuity equation. Currently, the smaller among the two current densities is taken as the solar cell current density. This is particularly helpful to aid numerical optimization methods.
Arguments:
- voltage: Voltage [$\mathrm{V}$].
Returns:
Current density [$\mathrm{mA \, cm^{-2}}$].
417 def power_den_vs_voltage(self, voltage: Real) -> float: 418 r"""Power density vs voltage. 419 420 Args: 421 voltage: Voltage [$\mathrm{V}$]. 422 423 Returns: 424 Power density [$\mathrm{mW \, cm^{-2}}$]. 425 """ 426 return voltage * self.curr_den_vs_voltage(voltage)
Power density vs voltage.
Arguments:
- voltage: Voltage [$\mathrm{V}$].
Returns:
Power density [$\mathrm{mW \, cm^{-2}}$].
428 def apply_voltage_sweep(self, voltage_sweep: Sequence[Real, ...]) -> None: 429 r"""Apply voltage sweep.""" 430 self._voltage_sweep = np.unique(voltage_sweep).astype(float) 431 self._db_model_sweep = None 432 self._curr_den_sweep = None 433 self._power_den_sweep = None 434 self._efficiency_sweep = None
Apply voltage sweep.
436 def clear_voltage_sweep(self) -> None: 437 r"""Clear voltage sweep.""" 438 self.apply_voltage_sweep([])
Clear voltage sweep.
440 def plot_rsv_sp_absorb_vs_energy( 441 self, 442 *, 443 energy_min: Real, 444 energy_max: Real, 445 energy_inc: Real, 446 ax: plt.Axes | None = None, 447 ) -> plt.Figure | None: 448 r"""Plot resolved spectral absorbance vs photon energy. 449 450 Args: 451 energy_min: Photon energy minimum [$\mathrm{eV}$]. 452 energy_max: Photon energy maximum [$\mathrm{eV}$]. 453 energy_inc: Photon energy increment [$\mathrm{eV}$]. 454 ax: Plot axes. If None, new figure and axes are created. 455 456 Returns: 457 Plot figure. 458 """ 459 return self.rsv_sp_absorb_ppoly.plot_rsv_sp_absorb_vs_energy( 460 energy_min=energy_min, 461 energy_max=energy_max, 462 energy_inc=energy_inc, 463 ax=ax, 464 )
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.
466 def plot_voltage_oc( 467 self, 468 *, 469 ax: plt.Axes | None = None, 470 color: str | None = "k", 471 label: str | None = None, 472 ) -> plt.Figure: 473 r"""Plot open circuit voltage. 474 475 Args: 476 ax: Plot axes. If None, new figure and axes are created. 477 color: Color. 478 label: Label. 479 480 Returns: 481 Plot figure. 482 """ 483 if ax is None: 484 fig, ax = plt.subplots(tight_layout=True) 485 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 486 else: 487 fig = ax.get_figure() 488 489 ax.axvline(self.voltage_oc, linestyle=":", color=color, label=label) 490 return fig
Plot open circuit voltage.
Arguments:
- ax: Plot axes. If None, new figure and axes are created.
- color: Color.
- label: Label.
Returns:
Plot figure.
492 def plot_curr_den_sc( 493 self, 494 *, 495 ax: plt.Axes | None = None, 496 color: str | None = "k", 497 label: str | None = None, 498 ) -> plt.Figure: 499 r"""Plot short circuit current density. 500 501 Args: 502 ax: Plot axes. If None, new figure and axes are created. 503 color: Color. 504 label: Label. 505 506 Returns: 507 Plot figure. 508 """ 509 if ax is None: 510 fig, ax = plt.subplots(tight_layout=True) 511 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 512 else: 513 fig = ax.get_figure() 514 515 ax.axhline(self.curr_den_sc, linestyle=":", color=color, label=label) 516 return fig
Plot short circuit current density.
Arguments:
- ax: Plot axes. If None, new figure and axes are created.
- color: Color.
- label: Label.
Returns:
Plot figure.
518 def plot_voltage_mpp( 519 self, 520 *, 521 ax: plt.Axes | None = None, 522 color: str | None = "k", 523 label: str | None = None, 524 ) -> plt.Figure: 525 r"""Plot maximum power point voltage. 526 527 Args: 528 ax: Plot axes. If None, new figure and axes are created. 529 color: Color. 530 label: Label. 531 532 Returns: 533 Plot figure. 534 """ 535 if ax is None: 536 fig, ax = plt.subplots(tight_layout=True) 537 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 538 else: 539 fig = ax.get_figure() 540 541 ax.axvline(self.voltage_mpp, linestyle=":", color=color, label=label) 542 return fig
Plot maximum power point voltage.
Arguments:
- ax: Plot axes. If None, new figure and axes are created.
- color: Color.
- label: Label.
Returns:
Plot figure.
544 def plot_curr_den_mpp( 545 self, 546 *, 547 ax: plt.Axes | None = None, 548 color: str | None = "k", 549 label: str | None = None, 550 ) -> plt.Figure: 551 r"""Plot maximum power point current density. 552 553 Args: 554 ax: Plot axes. If None, new figure and axes are created. 555 color: Color. 556 label: Label. 557 558 Returns: 559 Plot figure. 560 """ 561 if ax is None: 562 fig, ax = plt.subplots(tight_layout=True) 563 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 564 else: 565 fig = ax.get_figure() 566 567 ax.axhline(self.curr_den_mpp, linestyle=":", color=color, label=label) 568 return fig
Plot maximum power point current density.
Arguments:
- ax: Plot axes. If None, new figure and axes are created.
- color: Color.
- label: Label.
Returns:
Plot figure.
570 def plot_power_den_mpp( 571 self, 572 *, 573 ax: plt.Axes | None = None, 574 color: str | None = "k", 575 label: str | None = None, 576 ) -> plt.Figure: 577 r"""Plot maximum power point power density. 578 579 Args: 580 ax: Plot axes. If None, new figure and axes are created. 581 color: Color. 582 label: Label. 583 584 Returns: 585 Plot figure. 586 """ 587 if ax is None: 588 fig, ax = plt.subplots(tight_layout=True) 589 ax.set_ylabel(r"Power density / $\mathrm{mA \cdot cm^{-2}}$") 590 else: 591 fig = ax.get_figure() 592 593 ax.axhline(self.power_den_mpp, linestyle=":", color=color, label=label) 594 return fig
Plot maximum power point power density.
Arguments:
- ax: Plot axes. If None, new figure and axes are created.
- color: Color.
- label: Label.
Returns:
Plot figure.
596 def plot_efficiency_mpp( 597 self, 598 *, 599 ax: plt.Axes | None = None, 600 color: str | None = "k", 601 label: str | None = None, 602 ) -> plt.Figure: 603 r"""Plot maximum power point efficiency. 604 605 Args: 606 ax: Plot axes. If None, new figure and axes are created. 607 color: Color. 608 label: Label. 609 610 Returns: 611 Plot figure. 612 """ 613 if ax is None: 614 fig, ax = plt.subplots(tight_layout=True) 615 ax.set_ylabel(r"Efficiency / $\mathrm{\%}$") 616 else: 617 fig = ax.get_figure() 618 619 ax.axhline(self.efficiency_mpp, linestyle=":", color=color, label=label) 620 return fig
Plot maximum power point efficiency.
Arguments:
- ax: Plot axes. If None, new figure and axes are created.
- color: Color.
- label: Label.
Returns:
Plot figure.
622 def plot_curr_den_vs_voltage( 623 self, 624 *, 625 ax: plt.Axes | None = None, 626 color: str | None = None, 627 label: str | None = None, 628 ) -> plt.Figure: 629 r"""Plot current density vs voltage. 630 631 Args: 632 color: Color. 633 label: Label. 634 ax: Plot axes. If None, new figure and axes are created. 635 636 Returns: 637 Plot figure. 638 """ 639 if ax is None: 640 fig, ax = plt.subplots(tight_layout=True) 641 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 642 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 643 ax.set_ylim(ceil(self.curr_den_sc / 10) * 10 - 20, 0.0) 644 else: 645 fig = ax.get_figure() 646 647 ax.plot( 648 self.voltage_sweep, 649 self.curr_den_sweep, 650 color=color, 651 label=label, 652 ) 653 return fig
Plot current density vs voltage.
Arguments:
- color: Color.
- label: Label.
- ax: Plot axes. If None, new figure and axes are created.
Returns:
Plot figure.
655 def plot_power_den_vs_voltage( 656 self, 657 *, 658 ax: plt.Axes | None = None, 659 color: str | None = None, 660 label: str | None = None, 661 ) -> plt.Figure: 662 r"""Plot power density vs voltage. 663 664 Args: 665 ax: Plot axes. If None, new figure and axes are created. 666 color: Color. 667 label: Label. 668 669 Returns: 670 Plot figure. 671 """ 672 if ax is None: 673 fig, ax = plt.subplots(tight_layout=True) 674 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 675 ax.set_ylabel(r"Power density / $\mathrm{mW \cdot cm^{-2}}$") 676 ax.set_ylim(ceil(self.power_den_mpp / 10) * 10 - 20, 0.0) 677 else: 678 fig = ax.get_figure() 679 680 ax.plot( 681 self.voltage_sweep, 682 self.power_den_sweep, 683 color=color, 684 label=label, 685 ) 686 return fig
Plot power density vs voltage.
Arguments:
- ax: Plot axes. If None, new figure and axes are created.
- color: Color.
- label: Label.
Returns:
Plot figure.
688 def plot_efficiency_vs_voltage( 689 self, 690 *, 691 ax: plt.Axes | None = None, 692 color: str | None = None, 693 label: str | None = None, 694 ) -> plt.Figure: 695 r"""Plot efficiency vs voltage. 696 697 Args: 698 ax: Plot axes. If None, new figure and axes are created. 699 color: Color. 700 label: Label. 701 702 Returns: 703 Plot figure. 704 """ 705 if ax is None: 706 fig, ax = plt.subplots(tight_layout=True) 707 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 708 ax.set_ylabel(r"Efficiency / $\mathrm{\%}$") 709 ax.set_ylim(0.0, ceil(self.efficiency_mpp / 10) * 10 + 10) 710 else: 711 fig = ax.get_figure() 712 713 ax.plot( 714 self.voltage_sweep, 715 self.efficiency_sweep, 716 color=color, 717 label=label, 718 ) 719 return fig
Plot efficiency vs voltage.
Arguments:
- ax: Plot axes. If None, new figure and axes are created.
- color: Color.
- label: Label.
Returns:
Plot figure.
721 def plot_db_model_qty_mpp( 722 self, 723 qty: str, 724 *, 725 ax: plt.Axes | None = None, 726 color: str | None = "k", 727 label: str | None = None, 728 ) -> plt.Figure: 729 r"""Plot maximum power point detailed balance model quantity. 730 731 Args: 732 qty: Quantity, among 'potential_ii', 'potential_vi', 'potential_ic', 733 'potential_vc', 'phot_flux_ii', 'phot_flux_vi', 'phot_flux_ic', 734 'phot_flux_vc', 'curr_den_ib', 'curr_den_vb', and 'curr_den_cb'. 735 ax: Plot axes. If None, new figure and axes are created. 736 color: Color. 737 label: Label. 738 739 Returns: 740 Plot figure. 741 """ 742 if qty not in [ 743 "potential_ii", 744 "potential_vi", 745 "potential_ic", 746 "potential_vc", 747 "phot_flux_ii", 748 "phot_flux_vi", 749 "phot_flux_ic", 750 "phot_flux_vc", 751 "curr_den_ib", 752 "curr_den_vb", 753 "curr_den_cb", 754 ]: 755 raise ValueError("Quantity not supported!") 756 757 if ax is None: 758 fig, ax = plt.subplots(tight_layout=True) 759 if qty in [ 760 "potential_ii", 761 "potential_vi", 762 "potential_ic", 763 "potential_vc", 764 ]: 765 ax.set_ylabel(r"Chemical potential / $\mathrm{eV}$") 766 elif qty in [ 767 "phot_flux_ii", 768 "phot_flux_vi", 769 "phot_flux_ic", 770 "phot_flux_vc", 771 ]: 772 ax.set_ylabel(r"Photon flux / $\mathrm{s^{-1} \cdot cm^{-2}}$") 773 elif qty in [ 774 "curr_den_ib", 775 "curr_den_vb", 776 "curr_den_cb", 777 ]: 778 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 779 else: 780 fig = ax.get_figure() 781 782 qty_value = getattr(self.db_model_mpp, qty) 783 ax.axhline(qty_value, linestyle=":", color=color, label=label) 784 return fig
Plot maximum power point detailed balance model quantity.
Arguments:
- qty: Quantity, among 'potential_ii', 'potential_vi', 'potential_ic', 'potential_vc', 'phot_flux_ii', 'phot_flux_vi', 'phot_flux_ic', 'phot_flux_vc', 'curr_den_ib', 'curr_den_vb', and 'curr_den_cb'.
- ax: Plot axes. If None, new figure and axes are created.
- color: Color.
- label: Label.
Returns:
Plot figure.
786 def plot_db_model_qty_vs_voltage( 787 self, 788 qty: str, 789 *, 790 ax: plt.Axes | None = None, 791 color: str | None = None, 792 label: str | None = None, 793 ) -> plt.Figure: 794 r"""Plot detailed balance model quantity vs voltage. 795 796 Args: 797 qty: Quantity, among 'potential_ii', 'potential_vi', 'potential_ic', 798 'potential_vc', 'phot_flux_ii', 'phot_flux_vi', 'phot_flux_ic', 799 'phot_flux_vc', 'curr_den_ib', 'curr_den_vb', and 'curr_den_cb'. 800 ax: Plot axes. If None, new figure and axes are created. 801 802 Returns: 803 Plot figure. 804 """ 805 if qty not in [ 806 "potential_ii", 807 "potential_vi", 808 "potential_ic", 809 "potential_vc", 810 "phot_flux_ii", 811 "phot_flux_vi", 812 "phot_flux_ic", 813 "phot_flux_vc", 814 "curr_den_ib", 815 "curr_den_vb", 816 "curr_den_cb", 817 ]: 818 raise ValueError("Quantity not supported!") 819 820 if ax is None: 821 fig, ax = plt.subplots(tight_layout=True) 822 ax.set_xlabel(r"Voltage / $\mathrm{V}$") 823 if qty in [ 824 "potential_ii", 825 "potential_vi", 826 "potential_ic", 827 "potential_vc", 828 ]: 829 ax.set_ylabel(r"Chemical potential / $\mathrm{eV}$") 830 elif qty in [ 831 "phot_flux_ii", 832 "phot_flux_vi", 833 "phot_flux_ic", 834 "phot_flux_vc", 835 ]: 836 ax.set_ylabel(r"Photon flux / $\mathrm{s^{-1} \cdot cm^{-2}}$") 837 elif qty in [ 838 "curr_den_ib", 839 "curr_den_vb", 840 "curr_den_cb", 841 ]: 842 ax.set_ylabel(r"Current density / $\mathrm{mA \cdot cm^{-2}}$") 843 else: 844 fig = ax.get_figure() 845 846 qty_values = np.array( 847 [getattr(db_model, qty) for db_model in self.db_model_sweep], 848 dtype=float, 849 ) 850 ax.plot(self.voltage_sweep, qty_values, color=color, label=label) 851 return fig
Plot detailed balance model quantity vs voltage.
Arguments:
- qty: Quantity, among 'potential_ii', 'potential_vi', 'potential_ic', 'potential_vc', 'phot_flux_ii', 'phot_flux_vi', 'phot_flux_ic', 'phot_flux_vc', 'curr_den_ib', 'curr_den_vb', and 'curr_den_cb'.
- ax: Plot axes. If None, new figure and axes are created.
Returns:
Plot figure.
853 @classmethod 854 def sq1961( 855 cls, 856 *, 857 bandgap_vc: Real, 858 bandwidth_vc: Real, 859 temperature: Real, 860 concentration: Real, 861 ) -> Self | None: 862 r"""Shockley-Queisser solar cell at given temperature and concentration. 863 864 Note: 865 See https://dx.doi.org/10.1063%2F1.1736034 for details. 866 867 Args: 868 bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$]. 869 bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$]. 870 temperature: Temperature [$\mathrm{K}$]. 871 concentration: Sunlight concentration factor. 872 873 Returns: 874 Applied solar cell. 875 """ 876 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.sq1961( 877 bandgap_vc=bandgap_vc, 878 bandwidth_vc=bandwidth_vc, 879 ) 880 if rsv_sp_absorb_ppoly is None: 881 return None 882 else: 883 return cls( 884 rsv_sp_absorb_ppoly, 885 temperature=temperature, 886 concentration=concentration, 887 )
Shockley-Queisser solar cell at given temperature and concentration.
Note:
See https://dx.doi.org/10.1063%2F1.1736034 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}$].
- concentration: Sunlight concentration factor.
Returns:
Applied solar cell.
889 @classmethod 890 def lh2008( 891 cls, 892 *, 893 bandgap_vi: Real, 894 bandgap_ic: Real, 895 bandgap_vc: Real, 896 bandwidth_ii: Real, 897 bandwidth_vi: Real, 898 bandwidth_ic: Real, 899 bandwidth_vc: Real, 900 variant: str, 901 temperature: Real, 902 concentration: Real, 903 ) -> Self | None: 904 r"""Levi-Honsberg solar cell at given temperature and concentration. 905 906 Note: 907 See http://dx.doi.org/10.1103%2FPhysRevB.78.165122 for details. 908 909 Args: 910 bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$]. 911 bandgap_ic: Intermediate-conduction optical band gap 912 [$\mathrm{eV}$]. 913 bandgap_vc: Valence-conduction optical band gap 914 [$\mathrm{eV}$]. 915 bandwidth_ii: Intermediate-intermediate optical band width 916 [$\mathrm{eV}$]. 917 bandwidth_vi: Valence-intermediate optical band width 918 [$\mathrm{eV}$]. 919 bandwidth_ic: Intermediate-conduction optical band width 920 [$\mathrm{eV}$]. 921 bandwidth_vc: Valence-conduction optical band width 922 [$\mathrm{eV}$]. 923 variant: Model variant ('equal', 'inter', or 'intra'). 924 temperature: Temperature [$\mathrm{K}$]. 925 concentration: Sunlight concentration factor. 926 927 Returns: 928 Applied solar cell. 929 """ 930 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.lh2008( 931 bandgap_vi=bandgap_vi, 932 bandgap_ic=bandgap_ic, 933 bandgap_vc=bandgap_vc, 934 bandwidth_ii=bandwidth_ii, 935 bandwidth_vi=bandwidth_vi, 936 bandwidth_ic=bandwidth_ic, 937 bandwidth_vc=bandwidth_vc, 938 variant=variant, 939 ) 940 if rsv_sp_absorb_ppoly is None: 941 return None 942 else: 943 return cls( 944 rsv_sp_absorb_ppoly, 945 temperature=temperature, 946 concentration=concentration, 947 )
Levi-Honsberg solar cell at given temperature and concentration.
Note:
See http://dx.doi.org/10.1103%2FPhysRevB.78.165122 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}$].
- concentration: Sunlight concentration factor.
Returns:
Applied solar cell.
949 @classmethod 950 def from_data( 951 cls, 952 energy: Sequence[Real, ...], 953 rsv_sp_absorb: Mapping[str, Sequence[Real, ...]], 954 *, 955 temperature: Real, 956 concentration: Real, 957 ) -> Self | None: 958 r"""Solar cell at given temperature and concentration from data. 959 960 Args: 961 energy: Photon energy values [$\mathrm{eV}$]. 962 rsv_sp_absorb: Mapping of transition labels ('ii', 'vi', 'ic', 'vc') 963 into corresponding spectral absorbance values. 964 temperature: Temperature [$\mathrm{K}$]. 965 concentration: Sunlight concentration factor. 966 967 Returns: 968 Applied solar cell. 969 """ 970 rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.from_data( 971 energy, 972 rsv_sp_absorb, 973 ) 974 if rsv_sp_absorb_ppoly is None: 975 return None 976 else: 977 return cls( 978 rsv_sp_absorb_ppoly, 979 temperature=temperature, 980 concentration=concentration, 981 )
Solar cell at given temperature and concentration from data.
Arguments:
- energy: Photon energy values [$\mathrm{eV}$].
- rsv_sp_absorb: Mapping of transition labels ('ii', 'vi', 'ic', 'vc') into corresponding spectral absorbance values.
- temperature: Temperature [$\mathrm{K}$].
- concentration: Sunlight concentration factor.
Returns:
Applied solar cell.