scambio.balance

Solar cell detailed balance model.

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

Radiative exchange channel.

Arguments:
  • sp_absorb_ppoly: Spectral absorbance piecewise polynomial.
  • temperature: Temperature [$\mathrm{K}$].
  • potential: Chemical potential [$\mathrm{eV}$].
  • concentration: Sunlight concentration factor.
Raises:
  • TypeError: If sp_absorb_ppoly is not a SpectralAbsorbancePPoly object.
  • ValueError: If temperature, chemical potential, or sunlight concentration factor are unphysical.
RadiativeExchangeChannel( sp_absorb_ppoly: scambio.absorbance.SpectralAbsorbancePPoly, *, temperature: numbers.Real, potential: numbers.Real, concentration: numbers.Real)
32    def __init__(
33        self,
34        sp_absorb_ppoly: SpectralAbsorbancePPoly,
35        *,
36        temperature: Real,
37        potential: Real,
38        concentration: Real,
39    ) -> None:
40        r"""Initialize RadiativeExchangeChannel object."""
41        self.sp_absorb_ppoly = sp_absorb_ppoly
42        self.temperature = temperature
43        self.potential = potential
44        self.concentration = concentration

Initialize RadiativeExchangeChannel object.

sp_absorb_ppoly: scambio.absorbance.SpectralAbsorbancePPoly
46    @property
47    def sp_absorb_ppoly(self) -> SpectralAbsorbancePPoly:
48        r"""Spectral absorbance piecewise polynomial."""
49        return self._sp_absorb_ppoly

Spectral absorbance piecewise polynomial.

temperature: float
58    @property
59    def temperature(self) -> float:
60        r"""Temperature [$\mathrm{K}$]."""
61        return self._temperature

Temperature [$\mathrm{K}$].

potential: float
70    @property
71    def potential(self) -> float:
72        r"""Chemical potential [$\mathrm{eV}$]."""
73        return self._potential

Chemical potential [$\mathrm{eV}$].

concentration: float
82    @property
83    def concentration(self) -> float:
84        r"""Sunlight concentration factor."""
85        return self._concentration

Sunlight concentration factor.

phot_flux: float
 96    @property
 97    def phot_flux(self) -> float:
 98        r"""Photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$]."""
 99        if self._phot_flux is None:
100            self._phot_flux = 0.0
101            if not np.isnan(self.sp_absorb_ppoly.onset):
102                for i, pcoeff_i in enumerate(self.sp_absorb_ppoly.pcoeff.T):
103                    energy_lb = self.sp_absorb_ppoly.energy[i]
104                    energy_ub = self.sp_absorb_ppoly.energy[i + 1]
105                    for j, pcoeff_ij in enumerate(pcoeff_i):
106                        if pcoeff_ij != 0.0:
107                            order = 2 + j
108                            self._phot_flux += pcoeff_ij * (
109                                bose_einstein_integr(
110                                    order,
111                                    energy_lb,
112                                    temperature=self.temperature,
113                                    potential=self.potential,
114                                )
115                                - bose_einstein_integr(
116                                    order,
117                                    energy_ub,
118                                    temperature=self.temperature,
119                                    potential=self.potential,
120                                )
121                            )
122                            if self.concentration != 0.0:
123                                self._phot_flux -= (
124                                    pcoeff_ij
125                                    * self.concentration
126                                    * Constant.fsun
127                                    * (
128                                        bose_einstein_integr(
129                                            order,
130                                            energy_lb,
131                                            temperature=Constant.tsun,
132                                            potential=Constant.usun,
133                                        )
134                                        - bose_einstein_integr(
135                                            order,
136                                            energy_ub,
137                                            temperature=Constant.tsun,
138                                            potential=Constant.usun,
139                                        )
140                                    )
141                                )
142        return self._phot_flux

Photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].

def clear_cached_properties(self) -> None:
144    def clear_cached_properties(self) -> None:
145        r"""Clear cached properties."""
146        self._phot_flux = None

Clear cached properties.

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

Solar cell detailed balance model.

Note:

Applicable to Shockley-Queisser and intermediate band solar cells.

Arguments:
  • rsv_sp_absorb_ppoly: Resolved spectral absorbance piecewise polynomials.
  • temperature: Temperature [$\mathrm{K}$].
  • voltage: Voltage [$\mathrm{V}$].
  • concentration: Sunlight concentration factor.
Raises:
  • TypeError: If rsv_sp_absorb_ppoly is not a ResolvedSpectralAbsorbancePPoly object.
  • ValueError: If temperature, voltage, or sunlight concentration factor are unphysical.
DetailedBalanceModel( rsv_sp_absorb_ppoly: scambio.absorbance.ResolvedSpectralAbsorbancePPoly, *, temperature: numbers.Real, voltage: numbers.Real, concentration: numbers.Real)
168    def __init__(
169        self,
170        rsv_sp_absorb_ppoly: ResolvedSpectralAbsorbancePPoly,
171        *,
172        temperature: Real,
173        voltage: Real,
174        concentration: Real,
175    ) -> None:
176        r"""Initialize RadiativeExchangeSystem object."""
177        self.rsv_sp_absorb_ppoly = rsv_sp_absorb_ppoly
178        self.temperature = temperature
179        self.voltage = voltage
180        self.concentration = concentration

Initialize RadiativeExchangeSystem object.

rsv_sp_absorb_ppoly: scambio.absorbance.ResolvedSpectralAbsorbancePPoly
182    @property
183    def rsv_sp_absorb_ppoly(self) -> ResolvedSpectralAbsorbancePPoly:
184        r"""Resolved spectral absorbance piecewise polynomials."""
185        return self._rsv_sp_absorb_ppoly

Resolved spectral absorbance piecewise polynomials.

temperature: float
193    @property
194    def temperature(self) -> float:
195        r"""Temperature [$\mathrm{K}$]."""
196        return self._temperature

Temperature [$\mathrm{K}$].

voltage: float
205    @property
206    def voltage(self) -> float:
207        r"""Voltage [$\mathrm{V}$]."""
208        return self._voltage

Voltage [$\mathrm{V}$].

concentration: float
217    @property
218    def concentration(self) -> float:
219        r"""Sunlight concentration factor."""
220        return self._concentration

Sunlight concentration factor.

voltage_ub: float
231    @property
232    def voltage_ub(self) -> float:
233        r"""Voltage upper bound [$\mathrm{V}$]."""
234        if self._voltage_ub is None:
235            sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi
236            sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic
237            if (
238                sp_absorb_vi is not None
239                and not np.isnan(sp_absorb_vi.onset)
240                and sp_absorb_ic is not None
241                and not np.isnan(sp_absorb_ic.onset)
242            ):
243                voltage_ub = sp_absorb_vi.onset + sp_absorb_ic.onset
244            else:
245                sp_absorb_vc = self.rsv_sp_absorb_ppoly.vc
246                if sp_absorb_vc is not None and not np.isnan(
247                    sp_absorb_vc.onset
248                ):
249                    voltage_ub = sp_absorb_vc.onset
250                else:
251                    voltage_ub = np.inf
252            self._voltage_ub = float(voltage_ub)
253        return self._voltage_ub

Voltage upper bound [$\mathrm{V}$].

potential_vi_lb: float
255    @property
256    def potential_vi_lb(self) -> float:
257        r"""Valence-intermediate chemical potential lower bound [$\mathrm{eV}$]."""
258        if self._potential_vi_lb is None:
259            sp_absorb_ic = self.rsv_sp_absorb_ppoly.ic
260            if self.voltage < self.voltage_ub and sp_absorb_ic is not None:
261                onset_ic = sp_absorb_ic.onset
262                self._potential_vi_lb = self.voltage - onset_ic
263            else:
264                self._potential_vi_lb = np.nan
265        return self._potential_vi_lb

Valence-intermediate chemical potential lower bound [$\mathrm{eV}$].

potential_vi_ub: float
267    @property
268    def potential_vi_ub(self) -> float:
269        r"""Valence-intermediate chemical potential upper bound [$\mathrm{eV}$]."""
270        if self._potential_vi_ub is None:
271            sp_absorb_vi = self.rsv_sp_absorb_ppoly.vi
272            if sp_absorb_vi is not None:
273                onset_vi = sp_absorb_vi.onset
274                self._potential_vi_ub = onset_vi
275            else:
276                self._potential_vi_ub = np.nan
277        return self._potential_vi_ub

Valence-intermediate chemical potential upper bound [$\mathrm{eV}$].

potential_ii: float
279    @property
280    def potential_ii(self) -> float:
281        r"""Intermediate-intermediate chemical potential [$\mathrm{eV}$]."""
282        if self._potential_ii is None:
283            if self.rsv_sp_absorb_ppoly.ii is None:
284                self._potential_ii = np.nan
285            else:
286                self._potential_ii = 0.0
287        return self._potential_ii

Intermediate-intermediate chemical potential [$\mathrm{eV}$].

potential_vi: float
289    @property
290    def potential_vi(self) -> float:
291        r"""Valence-intermediate chemical potential [$\mathrm{eV}$]."""
292        if self._potential_vi is None:
293            if self.voltage >= self.voltage_ub:
294                self._potential_vi = np.nan
295            elif np.isnan(self.potential_vi_lb) and np.isnan(
296                self.potential_vi_ub
297            ):
298                self._potential_vi = np.nan
299            else:
300                if np.isnan(self.potential_vi_lb):
301                    potential_vi_b = self.potential_vi_ub
302                    potential_vi_a = potential_vi_b - 0.1
303                    curr_den_ib_a = self.curr_den_ib_vs_potential_vi(
304                        potential_vi_a
305                    )
306                    while curr_den_ib_a <= 0.0:
307                        potential_vi_a -= 0.1
308                        curr_den_ib_a = self.curr_den_ib_vs_potential_vi(
309                            potential_vi_a
310                        )
311                elif np.isnan(self.potential_vi_ub):
312                    potential_vi_a = self.potential_vi_lb
313                    potential_vi_b = potential_vi_a + 0.1
314                    curr_den_ib_b = self.curr_den_ib_vs_potential_vi(
315                        potential_vi_b
316                    )
317                    while curr_den_ib_b >= 0.0:
318                        potential_vi_b += 0.1
319                        curr_den_ib_b = self.curr_den_ib_vs_potential_vi(
320                            potential_vi_b
321                        )
322                else:
323                    potential_vi_a = self.potential_vi_lb
324                    potential_vi_b = self.potential_vi_ub
325
326                def objective(potential_vi):
327                    if potential_vi == potential_vi_a:
328                        return np.inf
329                    elif potential_vi == potential_vi_b:
330                        return -np.inf
331                    else:
332                        return self.curr_den_ib_vs_potential_vi(potential_vi)
333
334                sol = root_scalar(
335                    objective,
336                    method="brentq",
337                    bracket=[potential_vi_a, potential_vi_b],
338                )
339                if not sol.converged:
340                    print(
341                        "Intermediate-valence chemical potential "
342                        + "did not converge!"
343                    )
344                    self._potential_vi = np.nan
345                else:
346                    self._potential_vi = sol.root
347
348        return self._potential_vi

Valence-intermediate chemical potential [$\mathrm{eV}$].

potential_ic: float
350    @property
351    def potential_ic(self) -> float:
352        r"""Intermediate-conduction chemical potential [$\mathrm{eV}$]."""
353        if self._potential_ic is None:
354            self._potential_ic = self.potential_vc - self.potential_vi
355        return self._potential_ic

Intermediate-conduction chemical potential [$\mathrm{eV}$].

potential_vc: float
357    @property
358    def potential_vc(self) -> float:
359        r"""Valence-conduction chemical potential [$\mathrm{eV}$]."""
360        if self._potential_vc is None:
361            if self.voltage >= self.voltage_ub:
362                self._potential_vc = np.nan
363            else:
364                self._potential_vc = self.voltage
365        return self._potential_vc

Valence-conduction chemical potential [$\mathrm{eV}$].

phot_flux_ii: float
367    @property
368    def phot_flux_ii(self) -> float:
369        r"""Intermediate-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$]."""
370        if self._phot_flux_ii is None:
371            if self.rsv_sp_absorb_ppoly.ii is None:
372                self._phot_flux_ii = 0.0
373            else:
374                self._phot_flux_ii = RadiativeExchangeChannel(
375                    self.rsv_sp_absorb_ppoly.ii,
376                    temperature=self.temperature,
377                    potential=self.potential_ii,
378                    concentration=self.concentration,
379                ).phot_flux
380        return self._phot_flux_ii

Intermediate-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].

phot_flux_vi: float
382    @property
383    def phot_flux_vi(self) -> float:
384        r"""Valence-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$]."""
385        if self._phot_flux_vi is None:
386            self._phot_flux_vi = self.phot_flux_vi_vs_potential_vi(
387                self.potential_vi
388            )
389        return self._phot_flux_vi

Valence-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].

phot_flux_ic: float
391    @property
392    def phot_flux_ic(self) -> float:
393        r"""Intermediate-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$]."""
394        if self._phot_flux_ic is None:
395            self._phot_flux_ic = self.phot_flux_ic_vs_potential_vi(
396                self.potential_vi
397            )
398        return self._phot_flux_ic

Intermediate-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].

phot_flux_vc: float
400    @property
401    def phot_flux_vc(self) -> float:
402        r"""Valence-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$]."""
403        if self._phot_flux_vc is None:
404            if self.rsv_sp_absorb_ppoly.vc is None:
405                self._phot_flux_vc = 0.0
406            else:
407                self._phot_flux_vc = RadiativeExchangeChannel(
408                    self.rsv_sp_absorb_ppoly.vc,
409                    temperature=self.temperature,
410                    potential=self.potential_vc,
411                    concentration=self.concentration,
412                ).phot_flux
413        return self._phot_flux_vc

Valence-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].

curr_den_ib: float
415    @property
416    def curr_den_ib(self) -> float:
417        r"""Intermediate-band current density [$\mathrm{mA \, cm^{-2}}$]."""
418        if self._curr_den_ib is None:
419            self._curr_den_ib = Constant.q * (
420                self.phot_flux_ic - self.phot_flux_vi
421            )
422        return self._curr_den_ib

Intermediate-band current density [$\mathrm{mA \, cm^{-2}}$].

curr_den_vb: float
424    @property
425    def curr_den_vb(self) -> float:
426        r"""Valence band current density [$\mathrm{mA \, cm^{-2}}$]."""
427        if self._curr_den_vb is None:
428            self._curr_den_vb = Constant.q * (
429                self.phot_flux_vc + self.phot_flux_vi
430            )
431        return self._curr_den_vb

Valence band current density [$\mathrm{mA \, cm^{-2}}$].

curr_den_cb: float
433    @property
434    def curr_den_cb(self) -> float:
435        r"""Conduction band current density [$\mathrm{mA \, cm^{-2}}$]."""
436        if self._curr_den_cb is None:
437            self._curr_den_cb = Constant.q * (
438                self.phot_flux_vc + self.phot_flux_ic
439            )
440        return self._curr_den_cb

Conduction band current density [$\mathrm{mA \, cm^{-2}}$].

def clear_cached_properties(self) -> None:
442    def clear_cached_properties(self) -> None:
443        r"""Clear cached properties."""
444        self._voltage_ub = None
445        self._potential_vi_lb = None
446        self._potential_vi_ub = None
447        self._potential_ii = None
448        self._potential_vi = None
449        self._potential_ic = None
450        self._potential_vc = None
451        self._phot_flux_ii = None
452        self._phot_flux_vi = None
453        self._phot_flux_ic = None
454        self._phot_flux_vc = None
455        self._curr_den_ib = None
456        self._curr_den_vb = None
457        self._curr_den_cb = None

Clear cached properties.

def phot_flux_vi_vs_potential_vi(self, potential_vi: numbers.Real) -> float:
459    def phot_flux_vi_vs_potential_vi(self, potential_vi: Real) -> float:
460        r"""Valence-intermediate photon flux density vs chemical potential.
461
462        Args:
463            potential_vi: Valence-intermediate chemical potential
464                [$\mathrm{eV}$].
465
466        Returns:
467            Valence-intermediate photon flux density
468            [$\mathrm{s^{-1} \, cm^{-2}}$].
469        """
470        if self.rsv_sp_absorb_ppoly.vi is None:
471            return 0.0
472        else:
473            return RadiativeExchangeChannel(
474                self.rsv_sp_absorb_ppoly.vi,
475                temperature=self.temperature,
476                potential=potential_vi,
477                concentration=self.concentration,
478            ).phot_flux

Valence-intermediate photon flux density vs chemical potential.

Arguments:
  • potential_vi: Valence-intermediate chemical potential [$\mathrm{eV}$].
Returns:

Valence-intermediate photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].

def phot_flux_ic_vs_potential_vi(self, potential_vi: numbers.Real) -> float:
480    def phot_flux_ic_vs_potential_vi(self, potential_vi: Real) -> float:
481        r"""Intermediate-conduction photon flux density vs valence-intermediate chemical potential.
482
483        Args:
484            potential_vi: Valence-intermediate chemical potential [$\mathrm{eV}$].
485
486        Returns:
487            Intermediate-conduction photon flux density
488            [$\mathrm{s^{-1} \, cm^{-2}}$].
489        """
490        if self.rsv_sp_absorb_ppoly.ic is None:
491            return 0.0
492        else:
493            return RadiativeExchangeChannel(
494                self.rsv_sp_absorb_ppoly.ic,
495                temperature=self.temperature,
496                potential=self.potential_vc - potential_vi,
497                concentration=self.concentration,
498            ).phot_flux

Intermediate-conduction photon flux density vs valence-intermediate chemical potential.

Arguments:
  • potential_vi: Valence-intermediate chemical potential [$\mathrm{eV}$].
Returns:

Intermediate-conduction photon flux density [$\mathrm{s^{-1} \, cm^{-2}}$].

def curr_den_ib_vs_potential_vi(self, potential_vi: numbers.Real):
500    def curr_den_ib_vs_potential_vi(self, potential_vi: Real):
501        r"""Intermediate band current density vs vi chemical potential.
502
503        Args:
504            potential_vi: Valence-intermediate chemical potential
505                [$\mathrm{eV}$].
506
507        Returns:
508            Intermediate band current density [$\mathrm{mA \, cm^{-2}}$].
509        """
510        phot_flux_vi = self.phot_flux_vi_vs_potential_vi(potential_vi)
511        phot_flux_ic = self.phot_flux_ic_vs_potential_vi(potential_vi)
512        return Constant.q * (phot_flux_ic - phot_flux_vi)

Intermediate band current density vs vi chemical potential.

Arguments:
  • potential_vi: Valence-intermediate chemical potential [$\mathrm{eV}$].
Returns:

Intermediate band current density [$\mathrm{mA \, cm^{-2}}$].

@classmethod
def sq1961( cls, *, bandgap_vc: numbers.Real, bandwidth_vc: numbers.Real, temperature: numbers.Real, voltage: numbers.Real, concentration: numbers.Real) -> Optional[Self]:
514    @classmethod
515    def sq1961(
516        cls,
517        *,
518        bandgap_vc: Real,
519        bandwidth_vc: Real,
520        temperature: Real,
521        voltage: Real,
522        concentration: Real,
523    ) -> Self | None:
524        r"""Shockley-Queisser detailed balance model.
525
526        Note:
527            See [original article](https://dx.doi.org/10.1063%2F1.1736034) for
528            details.
529
530        Args:
531            bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
532            bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
533            temperature: Temperature [$\mathrm{K}$].
534            voltage: Voltage [$\mathrm{V}$].
535            concentration: Sunlight concentration factor.
536
537        Returns:
538            Detailed balance model.
539        """
540        rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.sq1961(
541            bandgap_vc=bandgap_vc,
542            bandwidth_vc=bandwidth_vc,
543        )
544        if rsv_sp_absorb_ppoly is None:
545            return None
546        else:
547            return cls(
548                rsv_sp_absorb_ppoly,
549                temperature=temperature,
550                voltage=voltage,
551                concentration=concentration,
552            )

Shockley-Queisser detailed balance model.

Note:

See original article for details.

Arguments:
  • bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
  • bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
  • temperature: Temperature [$\mathrm{K}$].
  • voltage: Voltage [$\mathrm{V}$].
  • concentration: Sunlight concentration factor.
Returns:

Detailed balance model.

@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, voltage: numbers.Real, concentration: numbers.Real) -> Optional[Self]:
554    @classmethod
555    def lh2008(
556        cls,
557        *,
558        bandgap_vi: Real,
559        bandgap_ic: Real,
560        bandgap_vc: Real,
561        bandwidth_ii: Real,
562        bandwidth_vi: Real,
563        bandwidth_ic: Real,
564        bandwidth_vc: Real,
565        variant: str,
566        temperature: Real,
567        voltage: Real,
568        concentration: Real,
569    ) -> Self | None:
570        r"""Levi-Honsberg detailed balance model.
571
572        Note:
573            See
574            [original article](http://dx.doi.org/10.1103%2FPhysRevB.78.165122)
575            for details.
576
577        Args:
578            bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$].
579            bandgap_ic: Intermediate-conduction optical band gap
580                [$\mathrm{eV}$].
581            bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
582            bandwidth_ii: Intermediate-intermediate optical band width
583                [$\mathrm{eV}$].
584            bandwidth_vi: Valence-intermediate optical band width
585                [$\mathrm{eV}$].
586            bandwidth_ic: Intermediate-conduction optical band width
587                [$\mathrm{eV}$].
588            bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
589            variant: Model variant ('equal', 'inter', or 'intra').
590            temperature: Temperature [$\mathrm{K}$].
591            voltage: Voltage [$\mathrm{V}$].
592            concentration: Sunlight concentration factor.
593
594        Returns:
595            Detailed balance model.
596        """
597        rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.lh2008(
598            bandgap_vi=bandgap_vi,
599            bandgap_ic=bandgap_ic,
600            bandgap_vc=bandgap_vc,
601            bandwidth_ii=bandwidth_ii,
602            bandwidth_vi=bandwidth_vi,
603            bandwidth_ic=bandwidth_ic,
604            bandwidth_vc=bandgap_vc,
605        )
606        if rsv_sp_absorb_ppoly is None:
607            return None
608        else:
609            return cls(
610                rsv_sp_absorb_ppoly,
611                temperature=temperature,
612                voltage=voltage,
613                concentration=concentration,
614            )

Levi-Honsberg detailed balance model.

Note:

See original article for details.

Arguments:
  • bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$].
  • bandgap_ic: Intermediate-conduction optical band gap [$\mathrm{eV}$].
  • bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
  • bandwidth_ii: Intermediate-intermediate optical band width [$\mathrm{eV}$].
  • bandwidth_vi: Valence-intermediate optical band width [$\mathrm{eV}$].
  • bandwidth_ic: Intermediate-conduction optical band width [$\mathrm{eV}$].
  • bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
  • variant: Model variant ('equal', 'inter', or 'intra').
  • temperature: Temperature [$\mathrm{K}$].
  • voltage: Voltage [$\mathrm{V}$].
  • concentration: Sunlight concentration factor.
Returns:

Detailed balance model.

@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, voltage: numbers.Real, concentration: numbers.Real) -> Self:
616    @classmethod
617    def from_data(
618        cls,
619        energy: Sequence[Real, ...],
620        rsv_sp_absorb: Mapping[str, Sequence[Real, ...]],
621        *,
622        temperature: Real,
623        voltage: Real,
624        concentration: Real,
625    ) -> Self:
626        r"""Create detailed balance model from data.
627
628        Args:
629            energy: Photon energy values [$\mathrm{eV}$].
630            rsv_sp_absorb: Mapping of band pair labels ('ii', 'vi', 'ic', 'vc')
631                into corresponding spectral absorbance values.
632            temperature: Temperature [$\mathrm{K}$].
633            voltage: Voltage [$\mathrm{V}$].
634            concentration: Sunlight concentration factor.
635
636        Returns:
637            Detailed balance model.
638        """
639        rsv_sp_absorb_ppoly = ResolvedSpectralAbsorbancePPoly.from_data(
640            energy,
641            rsv_sp_absorb,
642        )
643        if rsv_sp_absorb_ppoly is None:
644            return None
645        else:
646            return cls(
647                rsv_sp_absorb_ppoly,
648                temperature=temperature,
649                voltage=voltage,
650                concentration=concentration,
651            )

Create detailed balance model from data.

Arguments:
  • energy: Photon energy values [$\mathrm{eV}$].
  • rsv_sp_absorb: Mapping of band pair labels ('ii', 'vi', 'ic', 'vc') into corresponding spectral absorbance values.
  • temperature: Temperature [$\mathrm{K}$].
  • voltage: Voltage [$\mathrm{V}$].
  • concentration: Sunlight concentration factor.
Returns:

Detailed balance model.