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            )
class AppliedSolarCell:
 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.
AppliedSolarCell( rsv_sp_absorb_ppoly: scambio.absorbance.ResolvedSpectralAbsorbancePPoly, *, temperature: numbers.Real, concentration: numbers.Real)
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.

rsv_sp_absorb_ppoly: scambio.absorbance.ResolvedSpectralAbsorbancePPoly
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.

temperature: float
73    @property
74    def temperature(self) -> float:
75        r"""Temperature [$\mathrm{K}$]."""
76        return self._temperature

Temperature [$\mathrm{K}$].

concentration: float
85    @property
86    def concentration(self) -> float:
87        r"""Sunlight concentration factor."""
88        return self._concentration

Sunlight concentration factor.

onset_ii: float
 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}$].

offset_ii: float
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}$].

onset_vi: float
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}$].

offset_vi: float
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}$].

onset_ic: float
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}$].

offset_ic: float
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}$].

onset_vc: float
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}$].

offset_vc: float
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}$].

voltage_ub: float
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}$].

voltage_oc: float
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}$].

curr_den_sc: float
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}}$].

voltage_mpp: float
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}$].

db_model_mpp: scambio.balance.DetailedBalanceModel
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.

curr_den_mpp: float
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}}$].

power_den_mpp: float
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}}$].

efficiency_mpp: float
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{\%}$].

fill_factor: float
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.

voltage_sweep: numpy.ndarray[float]
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}$].

db_model_sweep: list[scambio.balance.DetailedBalanceModel, ...]
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.

curr_den_sweep: numpy.ndarray[float]
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}}$].

power_den_sweep: numpy.ndarray[float]
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}}$].

efficiency_sweep: numpy.ndarray[float]
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{\%}$].

def clear_cached_properties(self) -> None:
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.

def db_model_vs_voltage(self, voltage: numbers.Real) -> scambio.balance.DetailedBalanceModel:
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.

def curr_den_vs_voltage(self, voltage: numbers.Real) -> float:
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}}$].

def power_den_vs_voltage(self, voltage: numbers.Real) -> float:
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}}$].

def apply_voltage_sweep(self, voltage_sweep: collections.abc.Sequence[numbers.Real, ...]) -> None:
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.

def clear_voltage_sweep(self) -> None:
436    def clear_voltage_sweep(self) -> None:
437        r"""Clear voltage sweep."""
438        self.apply_voltage_sweep([])

Clear voltage sweep.

def plot_rsv_sp_absorb_vs_energy( self, *, energy_min: numbers.Real, energy_max: numbers.Real, energy_inc: numbers.Real, ax: matplotlib.axes._axes.Axes | None = None) -> matplotlib.figure.Figure | None:
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.

def plot_voltage_oc( self, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = 'k', label: str | None = None) -> matplotlib.figure.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.

def plot_curr_den_sc( self, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = 'k', label: str | None = None) -> matplotlib.figure.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.

def plot_voltage_mpp( self, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = 'k', label: str | None = None) -> matplotlib.figure.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.

def plot_curr_den_mpp( self, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = 'k', label: str | None = None) -> matplotlib.figure.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.

def plot_power_den_mpp( self, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = 'k', label: str | None = None) -> matplotlib.figure.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.

def plot_efficiency_mpp( self, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = 'k', label: str | None = None) -> matplotlib.figure.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.

def plot_curr_den_vs_voltage( self, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = None, label: str | None = None) -> matplotlib.figure.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.

def plot_power_den_vs_voltage( self, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = None, label: str | None = None) -> matplotlib.figure.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.

def plot_efficiency_vs_voltage( self, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = None, label: str | None = None) -> matplotlib.figure.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.

def plot_db_model_qty_mpp( self, qty: str, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = 'k', label: str | None = None) -> matplotlib.figure.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.

def plot_db_model_qty_vs_voltage( self, qty: str, *, ax: matplotlib.axes._axes.Axes | None = None, color: str | None = None, label: str | None = None) -> matplotlib.figure.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.

@classmethod
def sq1961( cls, *, bandgap_vc: numbers.Real, bandwidth_vc: numbers.Real, temperature: numbers.Real, concentration: numbers.Real) -> Optional[Self]:
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.

@classmethod
def lh2008( cls, *, bandgap_vi: numbers.Real, bandgap_ic: numbers.Real, bandgap_vc: numbers.Real, bandwidth_ii: numbers.Real, bandwidth_vi: numbers.Real, bandwidth_ic: numbers.Real, bandwidth_vc: numbers.Real, variant: str, temperature: numbers.Real, concentration: numbers.Real) -> Optional[Self]:
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.

@classmethod
def from_data( cls, energy: collections.abc.Sequence[numbers.Real, ...], rsv_sp_absorb: collections.abc.Mapping[str, collections.abc.Sequence[numbers.Real, ...]], *, temperature: numbers.Real, concentration: numbers.Real) -> Optional[Self]:
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.