scambio.absorbance

Solar cell absorbance.

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

Spectral absorbance piecewise polynomial.

Note:

Spectral absorbance is expressed as a $m$-th degree piecewise polynomial of the photon energy [$\mathrm{eV}$], specified through:

  • photon energy partition (1d-array):

    $$ \begin{bmatrix} \epsilon_{0} & \epsilon_{1} & \dots & \epsilon_{n + 1} \end{bmatrix} $$

    with $\epsilon_{i} < \epsilon_{i + 1}$.

  • polynomial coefficients for each partition interval (2d-array):

    $$ \begin{bmatrix} p_{0, 0} & p_{0, 1} & \dots & p_{0, n} \\ p_{1, 0} & p_{1, 1} & \dots & p_{1, n} \\ \dots & & & \\ p_{m, 0} & p_{m, 1} & \dots & p_{m, n} \end{bmatrix} $$

    so that:

    $$ A(\epsilon) = p_{0, i} + p_{1, i} \, \epsilon + \dots \ + p_{m, i} \, \epsilon^{m} $$

    for $\epsilon_{i} \le \epsilon \lt \epsilon_{i + 1}$

Arguments:
  • energy: Photon energy partition [$\mathrm{eV}$].
  • pcoeff: Polynomial coefficients [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$].
SpectralAbsorbancePPoly( energy: collections.abc.Sequence[numbers.Real, ...], pcoeff: collections.abc.Sequence[collections.abc.Sequence[numbers.Real, ...], ...])
63    def __init__(
64        self,
65        energy: Sequence[Real, ...],
66        pcoeff: Sequence[Sequence[Real, ...], ...],
67    ) -> None:
68        r"""Initialize SpectralAbsorbancePPoly object."""
69        self.set_energy_and_pcoeff(energy, pcoeff)

Initialize SpectralAbsorbancePPoly object.

energy: numpy.ndarray[float]
90    @property
91    def energy(self) -> np.ndarray[float]:
92        r"""Photon energy partition [$\mathrm{eV}$]."""
93        return self._energy

Photon energy partition [$\mathrm{eV}$].

pcoeff: numpy.ndarray[float]
95    @property
96    def pcoeff(self) -> np.ndarray[float]:
97        r"""Polynomial coefficients [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$]."""
98        return self._pcoeff

Polynomial coefficients [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$].

onset: float
100    @property
101    def onset(self) -> float:
102        r"""Absorption onset [$\mathrm{eV}$]."""
103        if self._onset is None:
104            self.set_onset_and_offset()
105        return self._onset

Absorption onset [$\mathrm{eV}$].

offset: float
107    @property
108    def offset(self) -> float:
109        r"""Absorption offset [$\mathrm{eV}$]."""
110        if self._offset is None:
111            self.set_onset_and_offset()
112        return self._offset

Absorption offset [$\mathrm{eV}$].

def clear_cached_properties(self) -> None:
114    def clear_cached_properties(self) -> None:
115        r"""Clear cached properties."""
116        self._onset = None
117        self._offset = None

Clear cached properties.

def set_energy_and_pcoeff( self, energy: collections.abc.Sequence[numbers.Real, ...], pcoeff: collections.abc.Sequence[collections.abc.Sequence[numbers.Real, ...], ...]) -> None:
119    def set_energy_and_pcoeff(
120        self,
121        energy: Sequence[Real, ...],
122        pcoeff: Sequence[Sequence[Real, ...], ...],
123    ) -> None:
124        r"""Set photon energy partition and polynomial coefficients.
125
126        Args:
127            energy: Photon energy partition [$\mathrm{eV}$].
128            pcoeff: Polynomial coefficients
129                [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$].
130
131        Raises:
132            ValueError: If photon energy partition is not valid, or piecewise
133                polynomial coefficients do not form a 2d-array or are not
134                commensurate with photon energy partition.
135        """
136        energy = np.asarray(energy, dtype=float)
137        if not np.array_equal(energy, np.unique(energy)):
138            raise ValueError("Invalid photon energy partition!")
139
140        pcoeff = np.asarray(pcoeff, dtype=float)
141        if not pcoeff.ndim == 2:
142            raise ValueError(
143                "Piecewise polynomial coefficients " + "do not form a 2d-array!"
144            )
145        if not pcoeff.shape[1] == energy.size - 1:
146            raise ValueError(
147                "Piecewise polynomial coefficients "
148                + "are not commensurate with photon energy partition!"
149            )
150
151        self._energy = energy
152        self._pcoeff = pcoeff
153        self.clear_cached_properties()

Set photon energy partition and polynomial coefficients.

Arguments:
  • energy: Photon energy partition [$\mathrm{eV}$].
  • pcoeff: Polynomial coefficients [$\mathrm{1, \dots, eV^{-j}, \dots, eV^{-m}}$].
Raises:
  • ValueError: If photon energy partition is not valid, or piecewise polynomial coefficients do not form a 2d-array or are not commensurate with photon energy partition.
def set_onset_and_offset(self) -> None:
155    def set_onset_and_offset(self) -> None:
156        r"""Set absorption onset and offset."""
157        has_nonzero_absorp = self.pcoeff.any(axis=0)
158        nonzero_absorp_i = np.argwhere(has_nonzero_absorp).flatten()
159        if not nonzero_absorp_i.size > 0:
160            self._onset = np.nan
161            self._offset = np.nan
162        else:
163            onset_i = nonzero_absorp_i[0]
164            offset_i = nonzero_absorp_i[-1] + 1
165            self._onset = self.energy[onset_i]
166            self._offset = self.energy[offset_i]

Set absorption onset and offset.

def eval_sp_absorb( self, energy: collections.abc.Sequence[numbers.Real, ...]) -> numpy.ndarray[float]:
168    def eval_sp_absorb(self, energy: Sequence[Real, ...]) -> np.ndarray[float]:
169        r"""Evaluate spectral absorbance at given photon energy values.
170
171        Args:
172            energy: Photon energy values [$\mathrm{eV}$].
173
174        Returns:
175            Spectral absorbance values.
176        """
177        energy = np.asarray(energy, dtype=float).flatten()
178        sp_absorb = np.array([], dtype=float)
179        for e in energy:
180            if e < np.amin(self.energy) or e >= np.amax(self.energy):
181                sp_absorb = np.append(sp_absorb, np.nan)
182            else:
183                i = np.searchsorted(self.energy, e, side="right")
184                pcoeff = self.pcoeff[:, i - 1]
185                sp_absorb = np.append(
186                    sp_absorb,
187                    np.polynomial.polynomial.polyval(e, pcoeff),
188                )
189        return sp_absorb

Evaluate spectral absorbance at given photon energy values.

Arguments:
  • energy: Photon energy values [$\mathrm{eV}$].
Returns:

Spectral absorbance values.

def plot_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:
191    def plot_sp_absorb_vs_energy(
192        self,
193        *,
194        energy_min: Real,
195        energy_max: Real,
196        energy_inc: Real,
197        ax: plt.Axes | None = None,
198    ) -> plt.Figure | None:
199        r"""Plot spectral absorbance vs photon energy.
200
201        Args:
202            energy_min: Photon energy minimum [$\mathrm{eV}$].
203            energy_max: Photon energy maximum [$\mathrm{eV}$].
204            energy_inc: Photon energy increment [$\mathrm{eV}$].
205            ax: Plot axes. If None, new figure and axes are created.
206
207        Returns:
208            Plot figure.
209        """
210        if not 0.0 <= energy_min < energy_max < np.inf:
211            print("Photon energy range is not valid!")
212            return None
213
214        if not 0.0 < energy_inc < energy_max - energy_min:
215            print("Photon energy increment is not valid!")
216            return None
217
218        energy = np.arange(energy_min, energy_max + energy_inc, energy_inc)
219        sp_absorb = self.eval_sp_absorb(energy)
220
221        if ax is None:
222            fig, ax = plt.subplots(tight_layout=True)
223            ax.set_xlabel(r"Photon energy / eV")
224            ax.set_ylabel(r"Spectral absorbance")
225            ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.8, 1.0])
226        else:
227            fig = ax.get_figure()
228
229        ax.plot(energy, sp_absorb, color="k")
230        ax.fill_between(energy, sp_absorb, alpha=0.5)
231        return fig

Plot spectral absorbance vs photon energy.

Arguments:
  • energy_min: Photon energy minimum [$\mathrm{eV}$].
  • energy_max: Photon energy maximum [$\mathrm{eV}$].
  • energy_inc: Photon energy increment [$\mathrm{eV}$].
  • ax: Plot axes. If None, new figure and axes are created.
Returns:

Plot figure.

@classmethod
def from_data( cls, energy: collections.abc.Sequence[numbers.Real, ...], sp_absorb: collections.abc.Sequence[numbers.Real, ...]) -> Self:
233    @classmethod
234    def from_data(
235        cls,
236        energy: Sequence[Real, ...],
237        sp_absorb: Sequence[Real, ...],
238    ) -> Self:
239        r"""Create SpectralAbsorbancePPoly from data.
240
241        Args:
242            energy: Photon energy values [$\mathrm{eV}$].
243            sp_absorb: Spectral absorbance values.
244
245        Returns:
246            Spectral absorbance piecewise polynomial.
247
248        Raises:
249            ValueError: If there are duplicate photon energy values.
250        """
251        energy = np.asarray(energy, dtype=float).flatten()
252        sp_absorb = np.asarray(sp_absorb, dtype=float).flatten()
253
254        sort_i = np.argsort(energy)
255        energy = energy[sort_i]
256        sp_absorb = sp_absorb[sort_i]
257        sp_absorb[np.isclose(sp_absorb, 0.0)] = 0.0
258
259        if not np.array_equal(energy, np.unique(energy)):
260            raise ValueError("Duplicate photon energy values!")
261
262        slope = np.diff(sp_absorb) / np.diff(energy)
263        offset = sp_absorb[:-1] - slope * energy[:-1]
264        pcoeff = np.asarray([offset, slope])
265
266        return cls(energy, pcoeff)

Create SpectralAbsorbancePPoly from data.

Arguments:
  • energy: Photon energy values [$\mathrm{eV}$].
  • sp_absorb: Spectral absorbance values.
Returns:

Spectral absorbance piecewise polynomial.

Raises:
  • ValueError: If there are duplicate photon energy values.
class ResolvedSpectralAbsorbancePPoly:
269class ResolvedSpectralAbsorbancePPoly:
270    r"""Resolved spectral absorbance piecewise polynomials.
271
272    Args:
273        ii: Intermediate-intermediate spectral absorbance.
274        vi: Valence-intermediate spectral absorbance.
275        ic: Intermediate-conduction spectral absorbance.
276        vc: Valence-conduction spectral absorbance.
277    """
278
279    def __init__(
280        self,
281        *,
282        ii: SpectralAbsorbancePPoly | Any = None,
283        vi: SpectralAbsorbancePPoly | Any = None,
284        ic: SpectralAbsorbancePPoly | Any = None,
285        vc: SpectralAbsorbancePPoly | Any = None,
286    ) -> None:
287        r"""Initialize ResolvedSpectralAbsorbancePPoly object."""
288        self.set_rsv_sp_absorb_ppoly(ii=ii, vi=vi, ic=ic, vc=vc)
289
290    def __repr__(self) -> str:
291        r"""Represent ResolvedSpectralAbsorbancePPoly object."""
292        blocks = []
293        if self.ii is not None:
294            blocks.append(f"{self.ii}".replace(":", " (ii):"))
295        if self.vi is not None:
296            blocks.append(f"{self.vi}".replace(":", " (vi):"))
297        if self.ic is not None:
298            blocks.append(f"{self.ic}".replace(":", " (ic):"))
299        if self.vc is not None:
300            blocks.append(f"{self.vc}".replace(":", " (vc):"))
301        return "\n\n".join(blocks)
302
303    @property
304    def ii(self) -> SpectralAbsorbancePPoly | None:
305        r"""Intermediate-intermediate spectral absorbance."""
306        return self._ii
307
308    @property
309    def vi(self) -> SpectralAbsorbancePPoly | None:
310        r"""Valence-intermediate spectral absorbance."""
311        return self._vi
312
313    @property
314    def ic(self) -> SpectralAbsorbancePPoly | None:
315        r"""Intermediate-conduction spectral absorbance."""
316        return self._ic
317
318    @property
319    def vc(self) -> SpectralAbsorbancePPoly | None:
320        r"""Valence-conduction spectral absorbance."""
321        return self._vc
322
323    def clear_cached_properties(self) -> None:
324        r"""Clear cached properties."""
325
326    def set_rsv_sp_absorb_ppoly(
327        self,
328        *,
329        ii: SpectralAbsorbancePPoly | Any = None,
330        vi: SpectralAbsorbancePPoly | Any = None,
331        ic: SpectralAbsorbancePPoly | Any = None,
332        vc: SpectralAbsorbancePPoly | Any = None,
333    ) -> None:
334        r"""Set resolved spectral absorbance piecewise polynomials.
335
336        Args:
337            ii: Intermediate-intermediate spectral absorbance.
338            vi: Valence-intermediate spectral absorbance.
339            ic: Intermediate-conduction spectral absorbance.
340            vc: Valence-conduction spectral absorbance.
341
342        Raises:
343            ValueError: If spectral absorbance piecewise polynomials do not
344                have the same photon energy partition or degree.
345        """
346        energy_xy_seq = []
347        pcoeff_xy_seq = []
348        for xy in [ii, vi, ic, vc]:
349            if isinstance(xy, SpectralAbsorbancePPoly):
350                energy_xy_seq.append(xy.energy)
351                pcoeff_xy_seq.append(xy.pcoeff)
352
353        for energy_xy in energy_xy_seq:
354            if not np.array_equal(energy_xy, energy_xy_seq[0]):
355                raise ValueError(
356                    "Spectral absorbance piecewise polynomials "
357                    + "must have the same photon energy partition!"
358                )
359
360        for pcoeff_xy in pcoeff_xy_seq:
361            if not pcoeff_xy.shape == pcoeff_xy_seq[0].shape:
362                raise ValueError(
363                    "Spectral absorbance piecewise polynomials "
364                    + "must have the same degree!"
365                )
366
367        self._ii = ii if isinstance(ii, SpectralAbsorbancePPoly) else None
368        self._vi = vi if isinstance(vi, SpectralAbsorbancePPoly) else None
369        self._ic = ic if isinstance(ic, SpectralAbsorbancePPoly) else None
370        self._vc = vc if isinstance(vc, SpectralAbsorbancePPoly) else None
371        self.clear_cached_properties()
372
373    def plot_rsv_sp_absorb_vs_energy(
374        self,
375        *,
376        energy_min: Real,
377        energy_max: Real,
378        energy_inc: Real,
379        ax: plt.Axes | None = None,
380    ) -> plt.Figure | None:
381        r"""Plot resolved spectral absorbance vs photon energy.
382
383        Args:
384            energy_min: Photon energy minimum [$\mathrm{eV}$].
385            energy_max: Photon energy maximum [$\mathrm{eV}$].
386            energy_inc: Photon energy increment [$\mathrm{eV}$].
387            ax: Plot axes. If None, new figure and axes are created.
388
389        Returns:
390            Plot figure.
391        """
392        if not 0.0 <= energy_min < energy_max < np.inf:
393            print("Photon energy range is not valid!")
394            return None
395
396        if not 0.0 < energy_inc < energy_max - energy_min:
397            print("Photon energy increment is not valid!")
398            return None
399
400        energy = np.arange(energy_min, energy_max + energy_inc, energy_inc)
401
402        rsv_sp_absorb = {}
403        for xy in ["ii", "vi", "ic", "vc"]:
404            sp_absorb_xy_ppoly = getattr(self, xy)
405            if sp_absorb_xy_ppoly is not None:
406                sp_absorb_xy = sp_absorb_xy_ppoly.eval_sp_absorb(energy)
407            else:
408                sp_absorb_xy = 0.0 * energy
409            rsv_sp_absorb.update({xy: sp_absorb_xy})
410
411        sp_absorb_tot = np.sum(
412            [sp_absorb_xy for sp_absorb_xy in rsv_sp_absorb.values()],
413            axis=0,
414        )
415
416        if ax is None:
417            fig, ax = plt.subplots(tight_layout=True)
418            ax.set_xlabel(r"Photon energy / $\mathrm{eV}$")
419            ax.set_ylabel(r"Spectral absorbance")
420            ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.8, 1.0])
421        else:
422            fig = ax.get_figure()
423
424        ax.plot(energy, sp_absorb_tot, color="k")
425        ax.stackplot(
426            energy,
427            rsv_sp_absorb.values(),
428            labels=rsv_sp_absorb.keys(),
429        )
430        ax.legend(
431            loc="lower center",
432            bbox_to_anchor=[0.5, 1.0],
433            frameon=False,
434            ncols=len(rsv_sp_absorb),
435        )
436        return fig
437
438    @classmethod
439    def sq1961(
440        cls,
441        *,
442        bandgap_vc: Real,
443        bandwidth_vc: Real,
444    ) -> Self | None:
445        r"""Shockley-Queisser resolved spectral absorbance piecewise polynomials.
446
447        Note:
448            See [original article](https://dx.doi.org/10.1063%2F1.1736034) for
449            details.
450
451        Args:
452            bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
453            bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
454
455        Returns:
456            Resolved spectral absorbance piecewise polynomial.
457        """
458        if not 0.0 < bandgap_vc < np.inf:
459            print("Valence-conduction optical band gap must be finite and > 0!")
460            return None
461
462        if not 0.0 <= bandwidth_vc <= np.inf:
463            print("Valence-conduction optical band width must be nonnegative!")
464            return None
465
466        onset = bandgap_vc
467        offset = bandgap_vc + bandwidth_vc
468        energy = np.unique([0.0, onset, offset, np.inf]).astype(float)
469
470        energy_mid = 0.5 * (energy[:-1] + energy[1:])
471        sp_absorp = rectangle(energy_mid, a=onset, b=offset)
472        pcoeff = sp_absorp.reshape(1, -1)
473
474        sp_absorb_ppoly = SpectralAbsorbancePPoly(energy, pcoeff)
475        return cls(vc=sp_absorb_ppoly)
476
477    @classmethod
478    def lh2008(
479        cls,
480        *,
481        bandgap_vi: Real,
482        bandgap_ic: Real,
483        bandgap_vc: Real,
484        bandwidth_ii: Real,
485        bandwidth_vi: Real,
486        bandwidth_ic: Real,
487        bandwidth_vc: Real,
488        variant: str,
489    ) -> Self | None:
490        r"""Levi-Honsberg resolved spectral absorbance piecewise polynomials.
491
492        Note:
493            See
494            [original article](http://dx.doi.org/10.1103%2FPhysRevB.78.165122)
495            for details.
496
497        Args:
498            bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$].
499            bandgap_ic: Intermediate-conduction optical band gap
500                [$\mathrm{eV}$].
501            bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
502            bandwidth_ii: Intermediate-intermediate optical band width
503                [$\mathrm{eV}$].
504            bandwidth_vi: Valence-intermediate optical band width
505                [$\mathrm{eV}$].
506            bandwidth_ic: Intermediate-conduction optical band width
507                [$\mathrm{eV}$].
508            bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
509            variant: Model variant ('equal', 'inter', or 'intra').
510
511        Returns:
512            Resolved spectral absorbance piecewise polynomials.
513        """
514        if not (
515            0.0 < bandgap_vi < np.inf
516            and 0.0 < bandgap_ic < np.inf
517            and 0.0 < bandgap_vc < np.inf
518        ):
519            print("Optical band gaps must be finite and positive!")
520            return None
521
522        if not (
523            bandwidth_ii >= 0.0
524            and bandwidth_vi >= 0.0
525            and bandwidth_ic >= 0.0
526            and bandwidth_vc >= 0.0
527        ):
528            print("Optical band widths must be nonnegative!")
529            return None
530
531        if not bandwidth_ii < np.inf:
532            print("Intermediate band width must be finite!")
533            return None
534
535        if not variant in {"equal", "inter", "intra"}:
536            print("Variant must be 'equal', 'inter' or 'intra'!")
537            return None
538
539        if not bandgap_vi + bandgap_ic + bandwidth_ii <= bandgap_vc:
540            if np.isclose(bandgap_vi + bandgap_ic + bandwidth_ii, bandgap_vc):
541                # print("Removing floating-point representation error...")
542                bandgap_vc = bandgap_vi + bandgap_ic + bandwidth_ii
543            else:
544                print("Valence-conduction optical bandgap is too small!")
545                return None
546
547        if variant == "inter":
548            onset = [0.0]
549            offset = [min(bandwidth_ii, bandgap_vi, bandgap_ic)]
550        else:
551            onset = [0.0]
552            offset = [bandwidth_ii]
553
554        if variant == "intra":
555            onset += [
556                max(bandgap_vi, bandwidth_ii),
557                max(bandgap_ic, bandwidth_ii),
558                max(bandgap_vc, bandwidth_ii),
559            ]
560            offset += [
561                max(bandgap_vi + bandwidth_vi, bandwidth_ii),
562                max(bandgap_ic + bandwidth_ic, bandwidth_ii),
563                max(bandgap_vc + bandwidth_vc, bandwidth_ii),
564            ]
565        else:
566            onset += [bandgap_vi, bandgap_ic, bandgap_vc]
567            offset += [
568                bandgap_vi + bandwidth_vi,
569                bandgap_ic + bandwidth_ic,
570                bandgap_vc + bandwidth_vc,
571            ]
572
573        energy = np.unique([0.0, np.inf] + onset + offset).astype(float)
574
575        energy_mid = 0.5 * (energy[:-1] + energy[1:])
576        sp_absorp = rectangle(energy_mid, a=onset, b=offset)
577        sp_absorp_tot = np.sum(sp_absorp, axis=0)
578        pcoeff = sp_absorp / np.where(sp_absorp_tot == 0.0, 1.0, sp_absorp_tot)
579
580        rsv_sp_absorb_ppoly = {
581            xy: SpectralAbsorbancePPoly(energy, pcoeff_xy.reshape(1, -1))
582            for xy, pcoeff_xy in zip(["ii", "vi", "ic", "vc"], pcoeff)
583        }
584        return cls(**rsv_sp_absorb_ppoly)
585
586    @classmethod
587    def from_data(
588        cls,
589        energy: Sequence[Real, ...],
590        rsv_sp_absorb: Mapping[str, Sequence[Real, ...]],
591    ) -> Self:
592        r"""Create resolved spectral absorbance piecewise polynomials from data.
593
594        Args:
595            energy: Photon energy values [$\mathrm{eV}$].
596            rsv_sp_absorb: Mapping of electronic transition labels ('ii', 'vi',
597                'ic', 'vc') into corresponding spectral absorbance values.
598
599        Returns:
600            Resolved spectral absorbace piecewise polynomials.
601        """
602        rsv_sp_absorb = {
603            xy: SpectralAbsorbancePPoly.from_data(energy, sp_absorb_xy)
604            for xy, sp_absorb_xy in rsv_sp_absorb.items()
605            if sp_absorb_xy is not None
606        }
607        return cls(**rsv_sp_absorb)

Resolved spectral absorbance piecewise polynomials.

Arguments:
  • ii: Intermediate-intermediate spectral absorbance.
  • vi: Valence-intermediate spectral absorbance.
  • ic: Intermediate-conduction spectral absorbance.
  • vc: Valence-conduction spectral absorbance.
ResolvedSpectralAbsorbancePPoly( *, ii: SpectralAbsorbancePPoly | typing.Any = None, vi: SpectralAbsorbancePPoly | typing.Any = None, ic: SpectralAbsorbancePPoly | typing.Any = None, vc: SpectralAbsorbancePPoly | typing.Any = None)
279    def __init__(
280        self,
281        *,
282        ii: SpectralAbsorbancePPoly | Any = None,
283        vi: SpectralAbsorbancePPoly | Any = None,
284        ic: SpectralAbsorbancePPoly | Any = None,
285        vc: SpectralAbsorbancePPoly | Any = None,
286    ) -> None:
287        r"""Initialize ResolvedSpectralAbsorbancePPoly object."""
288        self.set_rsv_sp_absorb_ppoly(ii=ii, vi=vi, ic=ic, vc=vc)

Initialize ResolvedSpectralAbsorbancePPoly object.

ii: SpectralAbsorbancePPoly | None
303    @property
304    def ii(self) -> SpectralAbsorbancePPoly | None:
305        r"""Intermediate-intermediate spectral absorbance."""
306        return self._ii

Intermediate-intermediate spectral absorbance.

vi: SpectralAbsorbancePPoly | None
308    @property
309    def vi(self) -> SpectralAbsorbancePPoly | None:
310        r"""Valence-intermediate spectral absorbance."""
311        return self._vi

Valence-intermediate spectral absorbance.

ic: SpectralAbsorbancePPoly | None
313    @property
314    def ic(self) -> SpectralAbsorbancePPoly | None:
315        r"""Intermediate-conduction spectral absorbance."""
316        return self._ic

Intermediate-conduction spectral absorbance.

vc: SpectralAbsorbancePPoly | None
318    @property
319    def vc(self) -> SpectralAbsorbancePPoly | None:
320        r"""Valence-conduction spectral absorbance."""
321        return self._vc

Valence-conduction spectral absorbance.

def clear_cached_properties(self) -> None:
323    def clear_cached_properties(self) -> None:
324        r"""Clear cached properties."""

Clear cached properties.

def set_rsv_sp_absorb_ppoly( self, *, ii: SpectralAbsorbancePPoly | typing.Any = None, vi: SpectralAbsorbancePPoly | typing.Any = None, ic: SpectralAbsorbancePPoly | typing.Any = None, vc: SpectralAbsorbancePPoly | typing.Any = None) -> None:
326    def set_rsv_sp_absorb_ppoly(
327        self,
328        *,
329        ii: SpectralAbsorbancePPoly | Any = None,
330        vi: SpectralAbsorbancePPoly | Any = None,
331        ic: SpectralAbsorbancePPoly | Any = None,
332        vc: SpectralAbsorbancePPoly | Any = None,
333    ) -> None:
334        r"""Set resolved spectral absorbance piecewise polynomials.
335
336        Args:
337            ii: Intermediate-intermediate spectral absorbance.
338            vi: Valence-intermediate spectral absorbance.
339            ic: Intermediate-conduction spectral absorbance.
340            vc: Valence-conduction spectral absorbance.
341
342        Raises:
343            ValueError: If spectral absorbance piecewise polynomials do not
344                have the same photon energy partition or degree.
345        """
346        energy_xy_seq = []
347        pcoeff_xy_seq = []
348        for xy in [ii, vi, ic, vc]:
349            if isinstance(xy, SpectralAbsorbancePPoly):
350                energy_xy_seq.append(xy.energy)
351                pcoeff_xy_seq.append(xy.pcoeff)
352
353        for energy_xy in energy_xy_seq:
354            if not np.array_equal(energy_xy, energy_xy_seq[0]):
355                raise ValueError(
356                    "Spectral absorbance piecewise polynomials "
357                    + "must have the same photon energy partition!"
358                )
359
360        for pcoeff_xy in pcoeff_xy_seq:
361            if not pcoeff_xy.shape == pcoeff_xy_seq[0].shape:
362                raise ValueError(
363                    "Spectral absorbance piecewise polynomials "
364                    + "must have the same degree!"
365                )
366
367        self._ii = ii if isinstance(ii, SpectralAbsorbancePPoly) else None
368        self._vi = vi if isinstance(vi, SpectralAbsorbancePPoly) else None
369        self._ic = ic if isinstance(ic, SpectralAbsorbancePPoly) else None
370        self._vc = vc if isinstance(vc, SpectralAbsorbancePPoly) else None
371        self.clear_cached_properties()

Set resolved spectral absorbance piecewise polynomials.

Arguments:
  • ii: Intermediate-intermediate spectral absorbance.
  • vi: Valence-intermediate spectral absorbance.
  • ic: Intermediate-conduction spectral absorbance.
  • vc: Valence-conduction spectral absorbance.
Raises:
  • ValueError: If spectral absorbance piecewise polynomials do not have the same photon energy partition or degree.
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:
373    def plot_rsv_sp_absorb_vs_energy(
374        self,
375        *,
376        energy_min: Real,
377        energy_max: Real,
378        energy_inc: Real,
379        ax: plt.Axes | None = None,
380    ) -> plt.Figure | None:
381        r"""Plot resolved spectral absorbance vs photon energy.
382
383        Args:
384            energy_min: Photon energy minimum [$\mathrm{eV}$].
385            energy_max: Photon energy maximum [$\mathrm{eV}$].
386            energy_inc: Photon energy increment [$\mathrm{eV}$].
387            ax: Plot axes. If None, new figure and axes are created.
388
389        Returns:
390            Plot figure.
391        """
392        if not 0.0 <= energy_min < energy_max < np.inf:
393            print("Photon energy range is not valid!")
394            return None
395
396        if not 0.0 < energy_inc < energy_max - energy_min:
397            print("Photon energy increment is not valid!")
398            return None
399
400        energy = np.arange(energy_min, energy_max + energy_inc, energy_inc)
401
402        rsv_sp_absorb = {}
403        for xy in ["ii", "vi", "ic", "vc"]:
404            sp_absorb_xy_ppoly = getattr(self, xy)
405            if sp_absorb_xy_ppoly is not None:
406                sp_absorb_xy = sp_absorb_xy_ppoly.eval_sp_absorb(energy)
407            else:
408                sp_absorb_xy = 0.0 * energy
409            rsv_sp_absorb.update({xy: sp_absorb_xy})
410
411        sp_absorb_tot = np.sum(
412            [sp_absorb_xy for sp_absorb_xy in rsv_sp_absorb.values()],
413            axis=0,
414        )
415
416        if ax is None:
417            fig, ax = plt.subplots(tight_layout=True)
418            ax.set_xlabel(r"Photon energy / $\mathrm{eV}$")
419            ax.set_ylabel(r"Spectral absorbance")
420            ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.8, 1.0])
421        else:
422            fig = ax.get_figure()
423
424        ax.plot(energy, sp_absorb_tot, color="k")
425        ax.stackplot(
426            energy,
427            rsv_sp_absorb.values(),
428            labels=rsv_sp_absorb.keys(),
429        )
430        ax.legend(
431            loc="lower center",
432            bbox_to_anchor=[0.5, 1.0],
433            frameon=False,
434            ncols=len(rsv_sp_absorb),
435        )
436        return fig

Plot resolved spectral absorbance vs photon energy.

Arguments:
  • energy_min: Photon energy minimum [$\mathrm{eV}$].
  • energy_max: Photon energy maximum [$\mathrm{eV}$].
  • energy_inc: Photon energy increment [$\mathrm{eV}$].
  • ax: Plot axes. If None, new figure and axes are created.
Returns:

Plot figure.

@classmethod
def sq1961( cls, *, bandgap_vc: numbers.Real, bandwidth_vc: numbers.Real) -> Optional[Self]:
438    @classmethod
439    def sq1961(
440        cls,
441        *,
442        bandgap_vc: Real,
443        bandwidth_vc: Real,
444    ) -> Self | None:
445        r"""Shockley-Queisser resolved spectral absorbance piecewise polynomials.
446
447        Note:
448            See [original article](https://dx.doi.org/10.1063%2F1.1736034) for
449            details.
450
451        Args:
452            bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
453            bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
454
455        Returns:
456            Resolved spectral absorbance piecewise polynomial.
457        """
458        if not 0.0 < bandgap_vc < np.inf:
459            print("Valence-conduction optical band gap must be finite and > 0!")
460            return None
461
462        if not 0.0 <= bandwidth_vc <= np.inf:
463            print("Valence-conduction optical band width must be nonnegative!")
464            return None
465
466        onset = bandgap_vc
467        offset = bandgap_vc + bandwidth_vc
468        energy = np.unique([0.0, onset, offset, np.inf]).astype(float)
469
470        energy_mid = 0.5 * (energy[:-1] + energy[1:])
471        sp_absorp = rectangle(energy_mid, a=onset, b=offset)
472        pcoeff = sp_absorp.reshape(1, -1)
473
474        sp_absorb_ppoly = SpectralAbsorbancePPoly(energy, pcoeff)
475        return cls(vc=sp_absorb_ppoly)

Shockley-Queisser resolved spectral absorbance piecewise polynomials.

Note:

See original article for details.

Arguments:
  • bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
  • bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
Returns:

Resolved spectral absorbance piecewise polynomial.

@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) -> Optional[Self]:
477    @classmethod
478    def lh2008(
479        cls,
480        *,
481        bandgap_vi: Real,
482        bandgap_ic: Real,
483        bandgap_vc: Real,
484        bandwidth_ii: Real,
485        bandwidth_vi: Real,
486        bandwidth_ic: Real,
487        bandwidth_vc: Real,
488        variant: str,
489    ) -> Self | None:
490        r"""Levi-Honsberg resolved spectral absorbance piecewise polynomials.
491
492        Note:
493            See
494            [original article](http://dx.doi.org/10.1103%2FPhysRevB.78.165122)
495            for details.
496
497        Args:
498            bandgap_vi: Valence-intermediate optical band gap [$\mathrm{eV}$].
499            bandgap_ic: Intermediate-conduction optical band gap
500                [$\mathrm{eV}$].
501            bandgap_vc: Valence-conduction optical band gap [$\mathrm{eV}$].
502            bandwidth_ii: Intermediate-intermediate optical band width
503                [$\mathrm{eV}$].
504            bandwidth_vi: Valence-intermediate optical band width
505                [$\mathrm{eV}$].
506            bandwidth_ic: Intermediate-conduction optical band width
507                [$\mathrm{eV}$].
508            bandwidth_vc: Valence-conduction optical band width [$\mathrm{eV}$].
509            variant: Model variant ('equal', 'inter', or 'intra').
510
511        Returns:
512            Resolved spectral absorbance piecewise polynomials.
513        """
514        if not (
515            0.0 < bandgap_vi < np.inf
516            and 0.0 < bandgap_ic < np.inf
517            and 0.0 < bandgap_vc < np.inf
518        ):
519            print("Optical band gaps must be finite and positive!")
520            return None
521
522        if not (
523            bandwidth_ii >= 0.0
524            and bandwidth_vi >= 0.0
525            and bandwidth_ic >= 0.0
526            and bandwidth_vc >= 0.0
527        ):
528            print("Optical band widths must be nonnegative!")
529            return None
530
531        if not bandwidth_ii < np.inf:
532            print("Intermediate band width must be finite!")
533            return None
534
535        if not variant in {"equal", "inter", "intra"}:
536            print("Variant must be 'equal', 'inter' or 'intra'!")
537            return None
538
539        if not bandgap_vi + bandgap_ic + bandwidth_ii <= bandgap_vc:
540            if np.isclose(bandgap_vi + bandgap_ic + bandwidth_ii, bandgap_vc):
541                # print("Removing floating-point representation error...")
542                bandgap_vc = bandgap_vi + bandgap_ic + bandwidth_ii
543            else:
544                print("Valence-conduction optical bandgap is too small!")
545                return None
546
547        if variant == "inter":
548            onset = [0.0]
549            offset = [min(bandwidth_ii, bandgap_vi, bandgap_ic)]
550        else:
551            onset = [0.0]
552            offset = [bandwidth_ii]
553
554        if variant == "intra":
555            onset += [
556                max(bandgap_vi, bandwidth_ii),
557                max(bandgap_ic, bandwidth_ii),
558                max(bandgap_vc, bandwidth_ii),
559            ]
560            offset += [
561                max(bandgap_vi + bandwidth_vi, bandwidth_ii),
562                max(bandgap_ic + bandwidth_ic, bandwidth_ii),
563                max(bandgap_vc + bandwidth_vc, bandwidth_ii),
564            ]
565        else:
566            onset += [bandgap_vi, bandgap_ic, bandgap_vc]
567            offset += [
568                bandgap_vi + bandwidth_vi,
569                bandgap_ic + bandwidth_ic,
570                bandgap_vc + bandwidth_vc,
571            ]
572
573        energy = np.unique([0.0, np.inf] + onset + offset).astype(float)
574
575        energy_mid = 0.5 * (energy[:-1] + energy[1:])
576        sp_absorp = rectangle(energy_mid, a=onset, b=offset)
577        sp_absorp_tot = np.sum(sp_absorp, axis=0)
578        pcoeff = sp_absorp / np.where(sp_absorp_tot == 0.0, 1.0, sp_absorp_tot)
579
580        rsv_sp_absorb_ppoly = {
581            xy: SpectralAbsorbancePPoly(energy, pcoeff_xy.reshape(1, -1))
582            for xy, pcoeff_xy in zip(["ii", "vi", "ic", "vc"], pcoeff)
583        }
584        return cls(**rsv_sp_absorb_ppoly)

Levi-Honsberg resolved spectral absorbance piecewise polynomials.

Note:

See original article for details.

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

Resolved spectral absorbance piecewise polynomials.

@classmethod
def from_data( cls, energy: collections.abc.Sequence[numbers.Real, ...], rsv_sp_absorb: collections.abc.Mapping[str, collections.abc.Sequence[numbers.Real, ...]]) -> Self:
586    @classmethod
587    def from_data(
588        cls,
589        energy: Sequence[Real, ...],
590        rsv_sp_absorb: Mapping[str, Sequence[Real, ...]],
591    ) -> Self:
592        r"""Create resolved spectral absorbance piecewise polynomials from data.
593
594        Args:
595            energy: Photon energy values [$\mathrm{eV}$].
596            rsv_sp_absorb: Mapping of electronic transition labels ('ii', 'vi',
597                'ic', 'vc') into corresponding spectral absorbance values.
598
599        Returns:
600            Resolved spectral absorbace piecewise polynomials.
601        """
602        rsv_sp_absorb = {
603            xy: SpectralAbsorbancePPoly.from_data(energy, sp_absorb_xy)
604            for xy, sp_absorb_xy in rsv_sp_absorb.items()
605            if sp_absorb_xy is not None
606        }
607        return cls(**rsv_sp_absorb)

Create resolved spectral absorbance piecewise polynomials from data.

Arguments:
  • energy: Photon energy values [$\mathrm{eV}$].
  • rsv_sp_absorb: Mapping of electronic transition labels ('ii', 'vi', 'ic', 'vc') into corresponding spectral absorbance values.
Returns:

Resolved spectral absorbace piecewise polynomials.