Skip to content

API Reference

This API documentation is generated from docstrings using mkdocstrings.

Devices

PN Junction diode device model.

DEFAULT_T = 300 module-attribute

k_B = 1.381e-23 module-attribute

q = 1.602e-19 module-attribute

Device(area=0.0001, temperature=DEFAULT_T)

Bases: ABC

Abstract base class for semiconductor devices.

Provides common fields and establishes a standard API for IV characteristics. Subclasses must implement iv_characteristic.

Source code in semiconductor_sim/devices/base.py
19
20
21
22
23
24
25
def __init__(self, area: float = 1e-4, temperature: float = DEFAULT_T) -> None:
    if not np.isfinite(area) or area <= 0:
        raise ValueError("area must be a positive finite value (cm^2)")
    if not np.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be a positive finite value (K)")
    self.area = area
    self.temperature = temperature

iv_characteristic(voltage_array, n_conc=None, p_conc=None) abstractmethod

Compute current vs. voltage. Implementations must return a tuple where the first element is the current array, and optional subsequent arrays include model-specific outputs (e.g., recombination rates, emission).

Source code in semiconductor_sim/devices/base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Compute current vs. voltage. Implementations must return a tuple where the
    first element is the current array, and optional subsequent arrays include
    model-specific outputs (e.g., recombination rates, emission).
    """
    raise NotImplementedError

Material(name, symbol, Eg0_eV, varshni_alpha_eV_per_K, varshni_beta_K, Nc_prefactor_cm3, Nv_prefactor_cm3) dataclass

PNJunctionDiode(doping_p, doping_n, area=0.0001, temperature=DEFAULT_T, tau_n=1e-06, tau_p=1e-06, D_n=25.0, D_p=10.0, L_n=0.0005, L_p=0.0005, material=None)

Bases: Device

PN Junction diode device model.

Assumptions: - Ideal diode equation with temperature-dependent I_s - SRH recombination with default mid-gap trap (n1≈p1≈n_i) - Default transport parameters (D, L) are representative constants - Units: cm, cm^2, cm^3, K; q in C, k_B in J/K

Initialize the PN Junction Diode.

Parameters:

Name Type Description Default
doping_p float

Acceptor concentration in p-region (cm^-3)

required
doping_n float

Donor concentration in n-region (cm^-3)

required
area float

Cross-sectional area of the diode (cm^2)

0.0001
temperature float

Temperature in Kelvin

DEFAULT_T
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06
Source code in semiconductor_sim/devices/pn_junction.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def __init__(
    self,
    doping_p: float,
    doping_n: float,
    area: float = 1e-4,
    temperature: float = DEFAULT_T,
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
    D_n: float = 25.0,
    D_p: float = 10.0,
    L_n: float = 5e-4,
    L_p: float = 5e-4,
    material: Material | None = None,
) -> None:
    """
    Initialize the PN Junction Diode.

    Parameters:
        doping_p: Acceptor concentration in p-region (cm^-3)
        doping_n: Donor concentration in n-region (cm^-3)
        area: Cross-sectional area of the diode (cm^2)
        temperature: Temperature in Kelvin
        tau_n: Electron lifetime (s)
        tau_p: Hole lifetime (s)
    """
    super().__init__(area=area, temperature=temperature)
    self.doping_p = float(doping_p)
    self.doping_n = float(doping_n)
    self.tau_n = float(tau_n)
    self.tau_p = float(tau_p)
    self.D_n = float(D_n)
    self.D_p = float(D_p)
    self.L_n = float(L_n)
    self.L_p = float(L_p)
    self.material = material
    self.I_s = self.calculate_saturation_current()

calculate_saturation_current()

Calculate the saturation current (I_s) considering temperature.

Source code in semiconductor_sim/devices/pn_junction.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def calculate_saturation_current(self) -> float:
    """Calculate the saturation current (I_s) considering temperature."""
    # Intrinsic carrier concentration with temperature dependence
    if self.material is not None:
        n_i = float(np.asarray(self.material.ni(self.temperature)))
    else:
        n_i = 1.5e10 * (self.temperature / DEFAULT_T) ** 1.5
    I_s = (
        q
        * self.area
        * n_i**2
        * ((self.D_p / (self.L_p * self.doping_n)) + (self.D_n / (self.L_n * self.doping_p)))
    )
    return float(I_s)

iv_characteristic(voltage_array, n_conc=None, p_conc=None)

Calculate the current for a given array of voltages, including SRH recombination.

Parameters:

Name Type Description Default
voltage_array NDArray[floating]

Array of voltage values (V)

required
n_conc float | NDArray[floating] | None

Electron concentration (cm^-3)

None
p_conc float | NDArray[floating] | None

Hole concentration (cm^-3)

None

Returns:

Type Description
tuple[NDArray[floating], NDArray[floating]]

Tuple of (current_array, recombination_array) matching the shape of voltage_array.

Source code in semiconductor_sim/devices/pn_junction.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
    """
    Calculate the current for a given array of voltages, including SRH recombination.

    Parameters:
        voltage_array: Array of voltage values (V)
        n_conc: Electron concentration (cm^-3)
        p_conc: Hole concentration (cm^-3)

    Returns:
        Tuple of `(current_array, recombination_array)` matching the shape of `voltage_array`.
    """
    V_T = k_B * self.temperature / q  # Thermal voltage
    I = self.I_s * safe_expm1(voltage_array / V_T)

    if n_conc is not None and p_conc is not None:
        R_SRH = srh_recombination(
            n_conc, p_conc, temperature=self.temperature, tau_n=self.tau_n, tau_p=self.tau_p
        )
        R_SRH = np.broadcast_to(R_SRH, np.shape(voltage_array))
    else:
        R_SRH = np.zeros_like(voltage_array)
    return np.asarray(I, dtype=float), np.asarray(R_SRH, dtype=float)

plot_iv_characteristic(voltage, current, recombination=None)

Plot the IV characteristics and optionally the recombination rate.

Source code in semiconductor_sim/devices/pn_junction.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def plot_iv_characteristic(
    self,
    voltage: npt.NDArray[np.floating],
    current: npt.NDArray[np.floating],
    recombination: npt.NDArray[np.floating] | None = None,
) -> None:
    """Plot the IV characteristics and optionally the recombination rate."""
    use_headless_backend("Agg")
    apply_basic_style()
    fig, ax1 = plt.subplots(figsize=(8, 6))

    color = "tab:blue"
    ax1.set_xlabel("Voltage (V)")
    ax1.set_ylabel("Current (A)", color=color)
    ax1.plot(voltage, current, color=color, label="IV Characteristic")
    ax1.tick_params(axis="y", labelcolor=color)
    ax1.grid(True)

    if recombination is not None:
        ax2 = ax1.twinx()
        color = "tab:green"
        ax2.set_ylabel("Recombination Rate (cm$^{-3}$ s$^{-1}$)", color=color)
        ax2.plot(voltage, recombination, color=color, label="SRH Recombination")
        ax2.tick_params(axis="y", labelcolor=color)

    fig.tight_layout()
    plt.title("PN Junction Diode IV Characteristics")
    plt.show()

apply_basic_style()

Apply a minimal style to keep plots consistent across devices.

Source code in semiconductor_sim/utils/plotting.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def apply_basic_style() -> None:
    """Apply a minimal style to keep plots consistent across devices."""
    with suppress(Exception):
        import matplotlib.pyplot as plt

        plt.rcParams.update(
            {
                "axes.grid": True,
                "axes.titlesize": 12,
                "axes.labelsize": 10,
                "legend.fontsize": 9,
                "figure.figsize": (8, 6),
            }
        )

safe_expm1(x, max_arg=700.0)

Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to avoid overflow and using numpy.expm1 for better precision near zero.

Parameters: - x: input value(s) - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

Returns: - np.ndarray: exp(x) - 1 computed safely

Source code in semiconductor_sim/utils/numerics.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def safe_expm1(
    x: npt.NDArray[np.floating] | float,
    max_arg: float = 700.0,
) -> npt.NDArray[np.float64]:
    """
    Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to
    avoid overflow and using numpy.expm1 for better precision near zero.

    Parameters:
    - x: input value(s)
    - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

    Returns:
    - np.ndarray: exp(x) - 1 computed safely
    """
    arr = np.asarray(x, dtype=float)
    clipped = np.clip(arr, -max_arg, max_arg)
    out = np.expm1(clipped)
    return cast(npt.NDArray[np.float64], out)

srh_recombination(n, p, temperature=float(DEFAULT_T), tau_n=1e-06, tau_p=1e-06, n1=None, p1=None)

Calculate the Shockley-Read-Hall (SRH) recombination rate.

Parameters:

Name Type Description Default
n float | NDArray[floating]

Electron concentration (cm^-3)

required
p float | NDArray[floating]

Hole concentration (cm^-3)

required
temperature float

Temperature in Kelvin

float(DEFAULT_T)
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06

Returns:

Type Description
float | NDArray[floating]

SRH recombination rate (cm^-3 s^-1).

Notes

Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default. Advanced users can override n1 and p1 to relax this assumption.

Source code in semiconductor_sim/models/recombination.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def srh_recombination(
    n: float | npt.NDArray[np.floating],
    p: float | npt.NDArray[np.floating],
    temperature: float = float(DEFAULT_T),
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
    n1: float | None = None,
    p1: float | None = None,
) -> float | npt.NDArray[np.floating]:
    """
    Calculate the Shockley-Read-Hall (SRH) recombination rate.

    Parameters:
        n: Electron concentration (cm^-3)
        p: Hole concentration (cm^-3)
        temperature: Temperature in Kelvin
        tau_n: Electron lifetime (s)
        tau_p: Hole lifetime (s)

    Returns:
        SRH recombination rate (cm^-3 s^-1).

    Notes:
        Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default.
        Advanced users can override `n1` and `p1` to relax this assumption.
    """
    n_i = 1.5e10 * (temperature / float(DEFAULT_T)) ** 1.5
    n1_val = n_i if n1 is None else n1
    p1_val = n_i if p1 is None else p1

    n_arr = np.asarray(n, dtype=float)
    p_arr = np.asarray(p, dtype=float)

    denominator = tau_p * (n_arr + n1_val) + tau_n * (p_arr + p1_val)
    R_SRH = (n_arr * p_arr - n_i**2) / denominator

    if np.isscalar(n) and np.isscalar(p):
        return float(np.asarray(R_SRH).item())
    return np.asarray(R_SRH, dtype=float)

use_headless_backend(preferred='Agg')

Switch Matplotlib backend to a non-interactive one if possible.

Parameters: - preferred: Backend name to use when switching (default: 'Agg').

Source code in semiconductor_sim/utils/plotting.py
10
11
12
13
14
15
16
17
18
def use_headless_backend(preferred: str = "Agg") -> None:
    """
    Switch Matplotlib backend to a non-interactive one if possible.

    Parameters:
    - preferred: Backend name to use when switching (default: 'Agg').
    """
    with suppress(Exception):
        matplotlib.use(preferred, force=True)

LED device model.

DEFAULT_T = 300 module-attribute

k_B = 1.381e-23 module-attribute

q = 1.602e-19 module-attribute

Device(area=0.0001, temperature=DEFAULT_T)

Bases: ABC

Abstract base class for semiconductor devices.

Provides common fields and establishes a standard API for IV characteristics. Subclasses must implement iv_characteristic.

Source code in semiconductor_sim/devices/base.py
19
20
21
22
23
24
25
def __init__(self, area: float = 1e-4, temperature: float = DEFAULT_T) -> None:
    if not np.isfinite(area) or area <= 0:
        raise ValueError("area must be a positive finite value (cm^2)")
    if not np.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be a positive finite value (K)")
    self.area = area
    self.temperature = temperature

iv_characteristic(voltage_array, n_conc=None, p_conc=None) abstractmethod

Compute current vs. voltage. Implementations must return a tuple where the first element is the current array, and optional subsequent arrays include model-specific outputs (e.g., recombination rates, emission).

Source code in semiconductor_sim/devices/base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Compute current vs. voltage. Implementations must return a tuple where the
    first element is the current array, and optional subsequent arrays include
    model-specific outputs (e.g., recombination rates, emission).
    """
    raise NotImplementedError

LED(doping_p, doping_n, area=0.0001, efficiency=0.1, temperature=DEFAULT_T, B=1e-10, D_n=25.0, D_p=10.0, L_n=0.0005, L_p=0.0005, material=None)

Bases: Device

Light-emitting diode (LED) model.

Assumptions: - Ideal diode IV: I = I_s * (exp(V/V_T) - 1) - Emission ~ efficiency * radiative_recombination * area (simplified) - Default transport parameters (D, L) represent typical pedagogical values - Units: cm, cm^2, cm^3, K; q in C, k_B in J/K

Initialize the LED device.

Parameters:

Name Type Description Default
doping_p float

Acceptor concentration in p-region (cm^-3)

required
doping_n float

Donor concentration in n-region (cm^-3)

required
area float

Cross-sectional area of the LED (cm^2)

0.0001
efficiency float

Radiative recombination efficiency (0 to 1)

0.1
temperature float

Temperature in Kelvin

DEFAULT_T
B float

Radiative recombination coefficient (cm^3/s)

1e-10
Source code in semiconductor_sim/devices/led.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(
    self,
    doping_p: float,
    doping_n: float,
    area: float = 1e-4,
    efficiency: float = 0.1,
    temperature: float = DEFAULT_T,
    B: float = 1e-10,
    D_n: float = 25.0,
    D_p: float = 10.0,
    L_n: float = 5e-4,
    L_p: float = 5e-4,
    material: Material | None = None,
) -> None:
    """
    Initialize the LED device.

    Parameters:
        doping_p (float): Acceptor concentration in p-region (cm^-3)
        doping_n (float): Donor concentration in n-region (cm^-3)
        area (float): Cross-sectional area of the LED (cm^2)
        efficiency (float): Radiative recombination efficiency (0 to 1)
        temperature (float): Temperature in Kelvin
        B (float): Radiative recombination coefficient (cm^3/s)
    """
    super().__init__(area=area, temperature=temperature)
    if not (0.0 <= efficiency <= 1.0):
        raise ValueError("efficiency must be between 0 and 1")
    self.doping_p = float(doping_p)
    self.doping_n = float(doping_n)
    self.efficiency = float(efficiency)
    self.B = float(B)  # Radiative recombination coefficient
    self.D_n = float(D_n)
    self.D_p = float(D_p)
    self.L_n = float(L_n)
    self.L_p = float(L_p)
    self.material = material
    self.I_s = self.calculate_saturation_current()

calculate_saturation_current()

Calculate the saturation current (I_s) considering temperature.

Returns:

Name Type Description
float float

The saturation current in amperes.

Source code in semiconductor_sim/devices/led.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def calculate_saturation_current(self) -> float:
    """
    Calculate the saturation current (I_s) considering temperature.

    Returns:
        float: The saturation current in amperes.
    """
    if self.material is not None:
        n_i = float(np.asarray(self.material.ni(self.temperature)))
    else:
        n_i = 1.5e10 * (self.temperature / DEFAULT_T) ** 1.5
    I_s = (
        q
        * self.area
        * n_i**2
        * ((self.D_p / (self.L_p * self.doping_n)) + (self.D_n / (self.L_n * self.doping_p)))
    )
    return float(I_s)

iv_characteristic(voltage_array, n_conc=None, p_conc=None)

Calculate current and optical emission across voltage_array.

Parameters:

Name Type Description Default
voltage_array NDArray[floating]

Array of voltage values (V).

required
n_conc float | NDArray[floating] | None

Electron concentration (cm^-3). If provided with p_conc, SRH and radiative recombination are computed and emission includes radiative term.

None
p_conc float | NDArray[floating] | None

Hole concentration (cm^-3).

Returns: - If both n_conc and p_conc are provided: (I, emission, R_SRH) where each is an array. - Else: (I, emission) where both are arrays.

None
Source code in semiconductor_sim/devices/led.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Calculate current and optical emission across `voltage_array`.

    Parameters:
        voltage_array: Array of voltage values (V).
        n_conc: Electron concentration (cm^-3). If provided with `p_conc`,
            SRH and radiative recombination are computed and emission includes radiative term.
        p_conc: Hole concentration (cm^-3).

            Returns:
                    - If both `n_conc` and `p_conc` are provided:
                        `(I, emission, R_SRH)` where each is an array.
                    - Else: `(I, emission)` where both are arrays.
    """
    V_T = k_B * self.temperature / q  # Thermal voltage
    I = self.I_s * safe_expm1(voltage_array / V_T)

    if n_conc is not None and p_conc is not None:
        R_SRH = srh_recombination(
            n_conc,
            p_conc,
            temperature=self.temperature,
            tau_n=1e-6,
            tau_p=1e-6,
        )
        R_rad = radiative_recombination(
            n_conc,
            p_conc,
            B=self.B,
            temperature=self.temperature,
        )
    else:
        R_SRH = np.zeros_like(voltage_array)
        R_rad = np.zeros_like(voltage_array)

    emission = self.efficiency * R_rad * self.area  # Simplified emission calculation
    I = np.asarray(I)
    emission = np.asarray(emission)
    if n_conc is not None and p_conc is not None:
        R_SRH = np.broadcast_to(R_SRH, np.shape(voltage_array))
        return I, emission, R_SRH
    return I, emission

plot_iv_characteristic(voltage, current, emission=None, recombination=None)

Plot the IV characteristics, emission intensity, and recombination rate.

Parameters:

Name Type Description Default
voltage NDArray[floating]

Voltage values (V)

required
current NDArray[floating]

Current values (A)

required
emission NDArray[floating] | None

Emission intensities (arb. units)

None
recombination NDArray[floating] | None

Recombination rates (cm^-3 s^-1)

None
Source code in semiconductor_sim/devices/led.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def plot_iv_characteristic(
    self,
    voltage: npt.NDArray[np.floating],
    current: npt.NDArray[np.floating],
    emission: npt.NDArray[np.floating] | None = None,
    recombination: npt.NDArray[np.floating] | None = None,
) -> None:
    """
    Plot the IV characteristics, emission intensity, and recombination rate.

    Parameters:
        voltage: Voltage values (V)
        current: Current values (A)
        emission: Emission intensities (arb. units)
        recombination: Recombination rates (cm^-3 s^-1)
    """
    fig = make_subplots(
        rows=2,
        cols=1,
        shared_xaxes=True,
        subplot_titles=("IV Characteristic", "Emission & Recombination"),
    )

    # IV Plot
    fig.add_trace(
        go.Scatter(
            x=voltage,
            y=current,
            mode='lines',
            name='IV Characteristic',
            line=dict(color='blue'),
        ),
        row=1,
        col=1,
    )
    if recombination is not None:
        fig.add_trace(
            go.Scatter(
                x=voltage,
                y=recombination,
                mode='lines',
                name='SRH Recombination',
                line=dict(color='green', dash='dash'),
            ),
            row=1,
            col=1,
        )

    # Emission Plot (Secondary y-axis)
    if emission is not None:
        fig.add_trace(
            go.Scatter(
                x=voltage,
                y=emission,
                mode='lines',
                name='Emission',
                line=dict(color='red', dash='dot'),
            ),
            row=1,
            col=1,
        )

    # Second subplot shows emission and/or recombination if provided
    if emission is not None:
        fig.add_trace(
            go.Scatter(
                x=voltage,
                y=emission,
                mode='lines',
                name='Emission',
                line=dict(color='red', dash='dot'),
            ),
            row=2,
            col=1,
        )
    if recombination is not None:
        fig.add_trace(
            go.Scatter(
                x=voltage,
                y=recombination,
                mode='lines',
                name='SRH Recombination',
                line=dict(color='green', dash='dash'),
            ),
            row=2,
            col=1,
        )

    fig.update_layout(height=800, width=800, title_text="LED IV, Emission & Recombination")
    fig.show()

Material(name, symbol, Eg0_eV, varshni_alpha_eV_per_K, varshni_beta_K, Nc_prefactor_cm3, Nv_prefactor_cm3) dataclass

safe_expm1(x, max_arg=700.0)

Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to avoid overflow and using numpy.expm1 for better precision near zero.

Parameters: - x: input value(s) - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

Returns: - np.ndarray: exp(x) - 1 computed safely

Source code in semiconductor_sim/utils/numerics.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def safe_expm1(
    x: npt.NDArray[np.floating] | float,
    max_arg: float = 700.0,
) -> npt.NDArray[np.float64]:
    """
    Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to
    avoid overflow and using numpy.expm1 for better precision near zero.

    Parameters:
    - x: input value(s)
    - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

    Returns:
    - np.ndarray: exp(x) - 1 computed safely
    """
    arr = np.asarray(x, dtype=float)
    clipped = np.clip(arr, -max_arg, max_arg)
    out = np.expm1(clipped)
    return cast(npt.NDArray[np.float64], out)

srh_recombination(n, p, temperature=float(DEFAULT_T), tau_n=1e-06, tau_p=1e-06, n1=None, p1=None)

Calculate the Shockley-Read-Hall (SRH) recombination rate.

Parameters:

Name Type Description Default
n float | NDArray[floating]

Electron concentration (cm^-3)

required
p float | NDArray[floating]

Hole concentration (cm^-3)

required
temperature float

Temperature in Kelvin

float(DEFAULT_T)
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06

Returns:

Type Description
float | NDArray[floating]

SRH recombination rate (cm^-3 s^-1).

Notes

Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default. Advanced users can override n1 and p1 to relax this assumption.

Source code in semiconductor_sim/models/recombination.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def srh_recombination(
    n: float | npt.NDArray[np.floating],
    p: float | npt.NDArray[np.floating],
    temperature: float = float(DEFAULT_T),
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
    n1: float | None = None,
    p1: float | None = None,
) -> float | npt.NDArray[np.floating]:
    """
    Calculate the Shockley-Read-Hall (SRH) recombination rate.

    Parameters:
        n: Electron concentration (cm^-3)
        p: Hole concentration (cm^-3)
        temperature: Temperature in Kelvin
        tau_n: Electron lifetime (s)
        tau_p: Hole lifetime (s)

    Returns:
        SRH recombination rate (cm^-3 s^-1).

    Notes:
        Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default.
        Advanced users can override `n1` and `p1` to relax this assumption.
    """
    n_i = 1.5e10 * (temperature / float(DEFAULT_T)) ** 1.5
    n1_val = n_i if n1 is None else n1
    p1_val = n_i if p1 is None else p1

    n_arr = np.asarray(n, dtype=float)
    p_arr = np.asarray(p, dtype=float)

    denominator = tau_p * (n_arr + n1_val) + tau_n * (p_arr + p1_val)
    R_SRH = (n_arr * p_arr - n_i**2) / denominator

    if np.isscalar(n) and np.isscalar(p):
        return float(np.asarray(R_SRH).item())
    return np.asarray(R_SRH, dtype=float)

Solar cell device model.

DEFAULT_T = 300 module-attribute

k_B = 1.381e-23 module-attribute

q = 1.602e-19 module-attribute

Device(area=0.0001, temperature=DEFAULT_T)

Bases: ABC

Abstract base class for semiconductor devices.

Provides common fields and establishes a standard API for IV characteristics. Subclasses must implement iv_characteristic.

Source code in semiconductor_sim/devices/base.py
19
20
21
22
23
24
25
def __init__(self, area: float = 1e-4, temperature: float = DEFAULT_T) -> None:
    if not np.isfinite(area) or area <= 0:
        raise ValueError("area must be a positive finite value (cm^2)")
    if not np.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be a positive finite value (K)")
    self.area = area
    self.temperature = temperature

iv_characteristic(voltage_array, n_conc=None, p_conc=None) abstractmethod

Compute current vs. voltage. Implementations must return a tuple where the first element is the current array, and optional subsequent arrays include model-specific outputs (e.g., recombination rates, emission).

Source code in semiconductor_sim/devices/base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Compute current vs. voltage. Implementations must return a tuple where the
    first element is the current array, and optional subsequent arrays include
    model-specific outputs (e.g., recombination rates, emission).
    """
    raise NotImplementedError

Material(name, symbol, Eg0_eV, varshni_alpha_eV_per_K, varshni_beta_K, Nc_prefactor_cm3, Nv_prefactor_cm3) dataclass

SolarCell(doping_p, doping_n, area=0.0001, light_intensity=1.0, temperature=DEFAULT_T, material=None)

Bases: Device

Initialize the Solar Cell device.

Parameters:

Name Type Description Default
doping_p float

Acceptor concentration in p-region (cm^-3)

required
doping_n float

Donor concentration in n-region (cm^-3)

required
area float

Cross-sectional area of the solar cell (cm^2)

0.0001
light_intensity float

Incident light intensity (arbitrary units)

1.0
temperature float

Temperature in Kelvin

DEFAULT_T
Source code in semiconductor_sim/devices/solar_cell.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def __init__(
    self,
    doping_p: float,
    doping_n: float,
    area: float = 1e-4,
    light_intensity: float = 1.0,
    temperature: float = DEFAULT_T,
    material: Material | None = None,
) -> None:
    """
    Initialize the Solar Cell device.

    Parameters:
        doping_p (float): Acceptor concentration in p-region (cm^-3)
        doping_n (float): Donor concentration in n-region (cm^-3)
        area (float): Cross-sectional area of the solar cell (cm^2)
        light_intensity (float): Incident light intensity (arbitrary units)
        temperature (float): Temperature in Kelvin
    """
    super().__init__(area=area, temperature=temperature)
    self.doping_p = doping_p
    self.doping_n = doping_n
    self.light_intensity = light_intensity
    self.material = material
    self.I_s = self.calculate_dark_saturation_current()
    self.I_sc = self.calculate_short_circuit_current()
    self.V_oc = self.calculate_open_circuit_voltage()

calculate_dark_saturation_current()

Calculate dark saturation current using material (if provided).

Source code in semiconductor_sim/devices/solar_cell.py
60
61
62
63
64
65
66
67
68
69
70
71
72
def calculate_dark_saturation_current(self) -> float:
    """Calculate dark saturation current using material (if provided)."""
    if self.material is not None:
        n_i = float(np.asarray(self.material.ni(self.temperature)))
    else:
        n_i = 1.5e10 * (self.temperature / DEFAULT_T) ** 1.5
    # Use representative transport constants as in PNJunction/LED for I_s form
    D_p, D_n = 10.0, 25.0
    L_p, L_n = 5e-4, 5e-4
    I_s = (
        q * self.area * n_i**2 * ((D_p / (L_p * self.doping_n)) + (D_n / (L_n * self.doping_p)))
    )
    return float(I_s)

calculate_open_circuit_voltage()

Calculate the open-circuit voltage (V_oc) using the diode equation.

Source code in semiconductor_sim/devices/solar_cell.py
52
53
54
55
56
57
58
def calculate_open_circuit_voltage(self) -> float:
    """
    Calculate the open-circuit voltage (V_oc) using the diode equation.
    """
    V_T = k_B * self.temperature / q
    V_oc = V_T * np.log((self.I_sc / max(self.I_s, 1e-30)) + 1)
    return float(V_oc)

calculate_short_circuit_current()

Calculate the short-circuit current (I_sc) based on light intensity.

Source code in semiconductor_sim/devices/solar_cell.py
44
45
46
47
48
49
50
def calculate_short_circuit_current(self) -> float:
    """
    Calculate the short-circuit current (I_sc) based on light intensity.
    """
    # Simplified assumption: I_sc proportional to light intensity
    I_sc = q * self.area * self.light_intensity * 1e12  # A
    return float(I_sc)

iv_characteristic(voltage_array, n_conc=None, p_conc=None)

Calculate the current for a given array of voltages under illumination.

Parameters:

Name Type Description Default
voltage_array NDArray[floating]

Array of voltage values (V)

required

Returns:

Type Description
NDArray[floating]

Tuple containing one element:

...
  • current_array: Array of current values (A)
Source code in semiconductor_sim/devices/solar_cell.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: npt.NDArray[np.floating] | float | None = None,
    p_conc: npt.NDArray[np.floating] | float | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Calculate the current for a given array of voltages under illumination.

    Parameters:
        voltage_array: Array of voltage values (V)

    Returns:
        Tuple containing one element:
        - current_array: Array of current values (A)
    """
    I = self.I_sc - self.I_s * safe_expm1(voltage_array / (k_B * self.temperature / q))
    return (np.asarray(I, dtype=float),)

plot_iv_characteristic(voltage, current)

Plot the IV characteristics of the solar cell.

Parameters:

Name Type Description Default
voltage NDArray[floating]

Voltage values (V)

required
current NDArray[floating]

Current values (A)

required
Source code in semiconductor_sim/devices/solar_cell.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def plot_iv_characteristic(
    self,
    voltage: npt.NDArray[np.floating],
    current: npt.NDArray[np.floating],
) -> None:
    """
    Plot the IV characteristics of the solar cell.

    Parameters:
        voltage: Voltage values (V)
        current: Current values (A)
    """
    use_headless_backend("Agg")
    apply_basic_style()
    plt.figure(figsize=(8, 6))
    plt.plot(voltage, current, label='Solar Cell IV')
    plt.title('Solar Cell IV Characteristics')
    plt.xlabel('Voltage (V)')
    plt.ylabel('Current (A)')
    plt.grid(True)
    plt.legend()
    plt.show()

apply_basic_style()

Apply a minimal style to keep plots consistent across devices.

Source code in semiconductor_sim/utils/plotting.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def apply_basic_style() -> None:
    """Apply a minimal style to keep plots consistent across devices."""
    with suppress(Exception):
        import matplotlib.pyplot as plt

        plt.rcParams.update(
            {
                "axes.grid": True,
                "axes.titlesize": 12,
                "axes.labelsize": 10,
                "legend.fontsize": 9,
                "figure.figsize": (8, 6),
            }
        )

safe_expm1(x, max_arg=700.0)

Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to avoid overflow and using numpy.expm1 for better precision near zero.

Parameters: - x: input value(s) - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

Returns: - np.ndarray: exp(x) - 1 computed safely

Source code in semiconductor_sim/utils/numerics.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def safe_expm1(
    x: npt.NDArray[np.floating] | float,
    max_arg: float = 700.0,
) -> npt.NDArray[np.float64]:
    """
    Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to
    avoid overflow and using numpy.expm1 for better precision near zero.

    Parameters:
    - x: input value(s)
    - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

    Returns:
    - np.ndarray: exp(x) - 1 computed safely
    """
    arr = np.asarray(x, dtype=float)
    clipped = np.clip(arr, -max_arg, max_arg)
    out = np.expm1(clipped)
    return cast(npt.NDArray[np.float64], out)

use_headless_backend(preferred='Agg')

Switch Matplotlib backend to a non-interactive one if possible.

Parameters: - preferred: Backend name to use when switching (default: 'Agg').

Source code in semiconductor_sim/utils/plotting.py
10
11
12
13
14
15
16
17
18
def use_headless_backend(preferred: str = "Agg") -> None:
    """
    Switch Matplotlib backend to a non-interactive one if possible.

    Parameters:
    - preferred: Backend name to use when switching (default: 'Agg').
    """
    with suppress(Exception):
        matplotlib.use(preferred, force=True)

Tunnel diode device model.

DEFAULT_T = 300 module-attribute

k_B = 1.381e-23 module-attribute

q = 1.602e-19 module-attribute

Device(area=0.0001, temperature=DEFAULT_T)

Bases: ABC

Abstract base class for semiconductor devices.

Provides common fields and establishes a standard API for IV characteristics. Subclasses must implement iv_characteristic.

Source code in semiconductor_sim/devices/base.py
19
20
21
22
23
24
25
def __init__(self, area: float = 1e-4, temperature: float = DEFAULT_T) -> None:
    if not np.isfinite(area) or area <= 0:
        raise ValueError("area must be a positive finite value (cm^2)")
    if not np.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be a positive finite value (K)")
    self.area = area
    self.temperature = temperature

iv_characteristic(voltage_array, n_conc=None, p_conc=None) abstractmethod

Compute current vs. voltage. Implementations must return a tuple where the first element is the current array, and optional subsequent arrays include model-specific outputs (e.g., recombination rates, emission).

Source code in semiconductor_sim/devices/base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Compute current vs. voltage. Implementations must return a tuple where the
    first element is the current array, and optional subsequent arrays include
    model-specific outputs (e.g., recombination rates, emission).
    """
    raise NotImplementedError

TunnelDiode(doping_p, doping_n, area=0.0001, temperature=DEFAULT_T, tau_n=1e-06, tau_p=1e-06)

Bases: Device

Initialize the Tunnel Diode.

Parameters:

Name Type Description Default
doping_p float

Acceptor concentration in p-region (cm^-3)

required
doping_n float

Donor concentration in n-region (cm^-3)

required
area float

Cross-sectional area of the diode (cm^2)

0.0001
temperature float

Temperature in Kelvin

DEFAULT_T
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06
Source code in semiconductor_sim/devices/tunnel_diode.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def __init__(
    self,
    doping_p: float,
    doping_n: float,
    area: float = 1e-4,
    temperature: float = DEFAULT_T,
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
) -> None:
    """
    Initialize the Tunnel Diode.

    Parameters:
        doping_p (float): Acceptor concentration in p-region (cm^-3)
        doping_n (float): Donor concentration in n-region (cm^-3)
        area (float): Cross-sectional area of the diode (cm^2)
        temperature (float): Temperature in Kelvin
        tau_n (float): Electron lifetime (s)
        tau_p (float): Hole lifetime (s)
    """
    super().__init__(area=area, temperature=temperature)
    self.doping_p = doping_p
    self.doping_n = doping_n
    self.tau_n = tau_n
    self.tau_p = tau_p
    self.I_s = self.calculate_saturation_current()
    self.Eg = 0.7  # Bandgap energy for Tunnel Diode (eV), adjust as needed

calculate_saturation_current()

Calculate the saturation current (I_s) considering temperature.

Source code in semiconductor_sim/devices/tunnel_diode.py
44
45
46
47
48
49
50
51
52
53
54
55
56
def calculate_saturation_current(self) -> float:
    """Calculate the saturation current (I_s) considering temperature."""
    # High doping concentrations lead to high I_s
    D_n = 30  # Electron diffusion coefficient (cm^2/s)
    D_p = 12  # Hole diffusion coefficient (cm^2/s)
    L_n = 1e-4  # Electron diffusion length (cm)
    L_p = 1e-4  # Hole diffusion length (cm)
    n_i = 1e10 * (self.temperature / DEFAULT_T) ** 1.5  # Intrinsic carrier concentration

    I_s = (
        q * self.area * n_i**2 * ((D_p / (L_p * self.doping_n)) + (D_n / (L_n * self.doping_p)))
    )
    return float(I_s)

iv_characteristic(voltage_array, n_conc=None, p_conc=None)

Calculate the current for a given array of voltages, including SRH recombination.

Parameters:

Name Type Description Default
voltage_array NDArray[floating]

Array of voltage values (V)

required
n_conc float | NDArray[floating] | None

Electron concentration (cm^-3)

None
p_conc float | NDArray[floating] | None

Hole concentration (cm^-3)

None

Returns:

Type Description
tuple[NDArray[floating], NDArray[floating]]

Tuple of (current_array, recombination_array) with shape matching voltage_array.

Source code in semiconductor_sim/devices/tunnel_diode.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
    """
    Calculate the current for a given array of voltages, including SRH recombination.

    Parameters:
        voltage_array: Array of voltage values (V)
        n_conc: Electron concentration (cm^-3)
        p_conc: Hole concentration (cm^-3)

    Returns:
        Tuple of `(current_array, recombination_array)` with shape matching `voltage_array`.
    """
    V_T = k_B * self.temperature / q  # Thermal voltage
    # Use a simplified exponential IV to ensure correct sign in reverse bias
    I = self.I_s * safe_expm1(voltage_array / V_T)

    if n_conc is not None and p_conc is not None:
        R_SRH = srh_recombination(
            n_conc, p_conc, temperature=self.temperature, tau_n=self.tau_n, tau_p=self.tau_p
        )
        R_SRH = np.broadcast_to(R_SRH, np.shape(voltage_array))
    else:
        R_SRH = np.zeros_like(voltage_array)

    return np.asarray(I), np.asarray(R_SRH)

plot_iv_characteristic(voltage, current, recombination=None)

Plot the IV characteristics and optionally the recombination rate.

Source code in semiconductor_sim/devices/tunnel_diode.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def plot_iv_characteristic(
    self,
    voltage: npt.NDArray[np.floating],
    current: npt.NDArray[np.floating],
    recombination: npt.NDArray[np.floating] | None = None,
) -> None:
    """Plot the IV characteristics and optionally the recombination rate."""
    use_headless_backend("Agg")
    apply_basic_style()
    fig, ax1 = plt.subplots(figsize=(8, 6))

    color = 'tab:blue'
    ax1.set_xlabel('Voltage (V)')
    ax1.set_ylabel('Current (A)', color=color)
    ax1.plot(voltage, current, color=color, label='IV Characteristic')
    ax1.tick_params(axis='y', labelcolor=color)
    ax1.grid(True)

    if recombination is not None:
        ax2 = ax1.twinx()
        color = 'tab:green'
        ax2.set_ylabel('Recombination Rate (cm$^{-3}$ s$^{-1}$)', color=color)
        ax2.plot(voltage, recombination, color=color, label='SRH Recombination')
        ax2.tick_params(axis='y', labelcolor=color)

    fig.tight_layout()
    plt.title('Tunnel Diode IV Characteristics')
    plt.show()

apply_basic_style()

Apply a minimal style to keep plots consistent across devices.

Source code in semiconductor_sim/utils/plotting.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def apply_basic_style() -> None:
    """Apply a minimal style to keep plots consistent across devices."""
    with suppress(Exception):
        import matplotlib.pyplot as plt

        plt.rcParams.update(
            {
                "axes.grid": True,
                "axes.titlesize": 12,
                "axes.labelsize": 10,
                "legend.fontsize": 9,
                "figure.figsize": (8, 6),
            }
        )

safe_expm1(x, max_arg=700.0)

Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to avoid overflow and using numpy.expm1 for better precision near zero.

Parameters: - x: input value(s) - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

Returns: - np.ndarray: exp(x) - 1 computed safely

Source code in semiconductor_sim/utils/numerics.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def safe_expm1(
    x: npt.NDArray[np.floating] | float,
    max_arg: float = 700.0,
) -> npt.NDArray[np.float64]:
    """
    Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to
    avoid overflow and using numpy.expm1 for better precision near zero.

    Parameters:
    - x: input value(s)
    - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

    Returns:
    - np.ndarray: exp(x) - 1 computed safely
    """
    arr = np.asarray(x, dtype=float)
    clipped = np.clip(arr, -max_arg, max_arg)
    out = np.expm1(clipped)
    return cast(npt.NDArray[np.float64], out)

srh_recombination(n, p, temperature=float(DEFAULT_T), tau_n=1e-06, tau_p=1e-06, n1=None, p1=None)

Calculate the Shockley-Read-Hall (SRH) recombination rate.

Parameters:

Name Type Description Default
n float | NDArray[floating]

Electron concentration (cm^-3)

required
p float | NDArray[floating]

Hole concentration (cm^-3)

required
temperature float

Temperature in Kelvin

float(DEFAULT_T)
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06

Returns:

Type Description
float | NDArray[floating]

SRH recombination rate (cm^-3 s^-1).

Notes

Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default. Advanced users can override n1 and p1 to relax this assumption.

Source code in semiconductor_sim/models/recombination.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def srh_recombination(
    n: float | npt.NDArray[np.floating],
    p: float | npt.NDArray[np.floating],
    temperature: float = float(DEFAULT_T),
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
    n1: float | None = None,
    p1: float | None = None,
) -> float | npt.NDArray[np.floating]:
    """
    Calculate the Shockley-Read-Hall (SRH) recombination rate.

    Parameters:
        n: Electron concentration (cm^-3)
        p: Hole concentration (cm^-3)
        temperature: Temperature in Kelvin
        tau_n: Electron lifetime (s)
        tau_p: Hole lifetime (s)

    Returns:
        SRH recombination rate (cm^-3 s^-1).

    Notes:
        Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default.
        Advanced users can override `n1` and `p1` to relax this assumption.
    """
    n_i = 1.5e10 * (temperature / float(DEFAULT_T)) ** 1.5
    n1_val = n_i if n1 is None else n1
    p1_val = n_i if p1 is None else p1

    n_arr = np.asarray(n, dtype=float)
    p_arr = np.asarray(p, dtype=float)

    denominator = tau_p * (n_arr + n1_val) + tau_n * (p_arr + p1_val)
    R_SRH = (n_arr * p_arr - n_i**2) / denominator

    if np.isscalar(n) and np.isscalar(p):
        return float(np.asarray(R_SRH).item())
    return np.asarray(R_SRH, dtype=float)

use_headless_backend(preferred='Agg')

Switch Matplotlib backend to a non-interactive one if possible.

Parameters: - preferred: Backend name to use when switching (default: 'Agg').

Source code in semiconductor_sim/utils/plotting.py
10
11
12
13
14
15
16
17
18
def use_headless_backend(preferred: str = "Agg") -> None:
    """
    Switch Matplotlib backend to a non-interactive one if possible.

    Parameters:
    - preferred: Backend name to use when switching (default: 'Agg').
    """
    with suppress(Exception):
        matplotlib.use(preferred, force=True)

Varactor diode device model.

DEFAULT_T = 300 module-attribute

k_B = 1.381e-23 module-attribute

q = 1.602e-19 module-attribute

Device(area=0.0001, temperature=DEFAULT_T)

Bases: ABC

Abstract base class for semiconductor devices.

Provides common fields and establishes a standard API for IV characteristics. Subclasses must implement iv_characteristic.

Source code in semiconductor_sim/devices/base.py
19
20
21
22
23
24
25
def __init__(self, area: float = 1e-4, temperature: float = DEFAULT_T) -> None:
    if not np.isfinite(area) or area <= 0:
        raise ValueError("area must be a positive finite value (cm^2)")
    if not np.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be a positive finite value (K)")
    self.area = area
    self.temperature = temperature

iv_characteristic(voltage_array, n_conc=None, p_conc=None) abstractmethod

Compute current vs. voltage. Implementations must return a tuple where the first element is the current array, and optional subsequent arrays include model-specific outputs (e.g., recombination rates, emission).

Source code in semiconductor_sim/devices/base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Compute current vs. voltage. Implementations must return a tuple where the
    first element is the current array, and optional subsequent arrays include
    model-specific outputs (e.g., recombination rates, emission).
    """
    raise NotImplementedError

VaractorDiode(doping_p, doping_n, area=0.0001, temperature=DEFAULT_T, tau_n=1e-06, tau_p=1e-06)

Bases: Device

Initialize the Varactor Diode.

Parameters:

Name Type Description Default
doping_p float

Acceptor concentration in p-region (cm^-3)

required
doping_n float

Donor concentration in n-region (cm^-3)

required
area float

Cross-sectional area of the diode (cm^2)

0.0001
temperature float

Temperature in Kelvin

DEFAULT_T
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06
Source code in semiconductor_sim/devices/varactor_diode.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(
    self,
    doping_p: float,
    doping_n: float,
    area: float = 1e-4,
    temperature: float = DEFAULT_T,
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
) -> None:
    """
    Initialize the Varactor Diode.

    Parameters:
        doping_p (float): Acceptor concentration in p-region (cm^-3)
        doping_n (float): Donor concentration in n-region (cm^-3)
        area (float): Cross-sectional area of the diode (cm^2)
        temperature (float): Temperature in Kelvin
        tau_n (float): Electron lifetime (s)
        tau_p (float): Hole lifetime (s)
    """
    super().__init__(area=area, temperature=temperature)
    self.doping_p = doping_p
    self.doping_n = doping_n
    self.tau_n = tau_n
    self.tau_p = tau_p
    self.I_s = self.calculate_saturation_current()

calculate_saturation_current()

Calculate the saturation current (I_s) considering temperature.

Source code in semiconductor_sim/devices/varactor_diode.py
43
44
45
46
47
48
49
50
51
52
53
54
def calculate_saturation_current(self) -> float:
    """Calculate the saturation current (I_s) considering temperature."""
    D_n = 25  # Electron diffusion coefficient (cm^2/s)
    D_p = 10  # Hole diffusion coefficient (cm^2/s)
    L_n = 5e-4  # Electron diffusion length (cm)
    L_p = 5e-4  # Hole diffusion length (cm)
    n_i = 1.5e10 * (self.temperature / DEFAULT_T) ** 1.5  # Intrinsic carrier concentration

    I_s = (
        q * self.area * n_i**2 * ((D_p / (L_p * self.doping_n)) + (D_n / (L_n * self.doping_p)))
    )
    return float(I_s)

capacitance(reverse_voltage)

Calculate the junction capacitance for a given reverse voltage.

Source code in semiconductor_sim/devices/varactor_diode.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def capacitance(
    self, reverse_voltage: float | npt.NDArray[np.floating]
) -> float | npt.NDArray[np.floating]:
    """Calculate the junction capacitance for a given reverse voltage."""
    # Permittivity of silicon (approx.)
    epsilon_s = 11.7 * 8.854e-14  # F/cm

    # Built-in potential (simplified)
    V_bi = 0.7  # Volts, adjust as needed

    # Calculate depletion width
    W = np.sqrt(
        (2 * epsilon_s * (V_bi + reverse_voltage))
        / (q * (self.doping_p + self.doping_n) / (self.doping_p * self.doping_n))
    )

    # Junction capacitance per unit area
    C_j_per_area = epsilon_s / W  # F/cm^2

    # Total junction capacitance
    C_j = C_j_per_area * self.area  # F

    return C_j

iv_characteristic(voltage_array, n_conc=None, p_conc=None)

Calculate current for voltage_array, including SRH recombination if concentrations are provided.

Returns (I, R_SRH) matching the shape of voltage_array.

Source code in semiconductor_sim/devices/varactor_diode.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
    """
    Calculate current for `voltage_array`, including SRH recombination
    if concentrations are provided.

    Returns `(I, R_SRH)` matching the shape of `voltage_array`.
    """
    V_T = k_B * self.temperature / q  # Thermal voltage
    I = self.I_s * safe_expm1(voltage_array / V_T)

    if n_conc is not None and p_conc is not None:
        R_SRH = srh_recombination(
            n_conc, p_conc, temperature=self.temperature, tau_n=self.tau_n, tau_p=self.tau_p
        )
        R_SRH = np.broadcast_to(R_SRH, np.shape(voltage_array))
    else:
        R_SRH = np.zeros_like(voltage_array)

    return np.asarray(I), np.asarray(R_SRH)

plot_capacitance_vs_voltage(voltage_array)

Plot the junction capacitance as a function of reverse voltage.

Source code in semiconductor_sim/devices/varactor_diode.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def plot_capacitance_vs_voltage(self, voltage_array: npt.NDArray[np.floating]) -> None:
    """Plot the junction capacitance as a function of reverse voltage."""
    C_j = self.capacitance(voltage_array)

    use_headless_backend("Agg")
    apply_basic_style()
    plt.figure(figsize=(8, 6))
    plt.plot(voltage_array, C_j, label='Junction Capacitance')
    plt.title('Varactor Diode Junction Capacitance vs. Reverse Voltage')
    plt.xlabel('Reverse Voltage (V)')
    plt.ylabel('Capacitance (F)')
    plt.grid(True)
    plt.legend()
    plt.show()

plot_iv_characteristic(voltage, current, recombination=None)

Plot the IV characteristics and optionally the recombination rate.

Source code in semiconductor_sim/devices/varactor_diode.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def plot_iv_characteristic(
    self,
    voltage: npt.NDArray[np.floating],
    current: npt.NDArray[np.floating],
    recombination: npt.NDArray[np.floating] | None = None,
) -> None:
    """Plot the IV characteristics and optionally the recombination rate."""
    use_headless_backend("Agg")
    apply_basic_style()
    fig, ax1 = plt.subplots(figsize=(8, 6))

    color = 'tab:blue'
    ax1.set_xlabel('Voltage (V)')
    ax1.set_ylabel('Current (A)', color=color)
    ax1.plot(voltage, current, color=color, label='IV Characteristic')
    ax1.tick_params(axis='y', labelcolor=color)
    ax1.grid(True)

    if recombination is not None:
        ax2 = ax1.twinx()
        color = 'tab:green'
        ax2.set_ylabel('Recombination Rate (cm$^{-3}$ s$^{-1}$)', color=color)
        ax2.plot(voltage, recombination, color=color, label='SRH Recombination')
        ax2.tick_params(axis='y', labelcolor=color)

    fig.tight_layout()
    plt.title('Varactor Diode IV Characteristics')
    plt.show()

apply_basic_style()

Apply a minimal style to keep plots consistent across devices.

Source code in semiconductor_sim/utils/plotting.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def apply_basic_style() -> None:
    """Apply a minimal style to keep plots consistent across devices."""
    with suppress(Exception):
        import matplotlib.pyplot as plt

        plt.rcParams.update(
            {
                "axes.grid": True,
                "axes.titlesize": 12,
                "axes.labelsize": 10,
                "legend.fontsize": 9,
                "figure.figsize": (8, 6),
            }
        )

safe_expm1(x, max_arg=700.0)

Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to avoid overflow and using numpy.expm1 for better precision near zero.

Parameters: - x: input value(s) - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

Returns: - np.ndarray: exp(x) - 1 computed safely

Source code in semiconductor_sim/utils/numerics.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def safe_expm1(
    x: npt.NDArray[np.floating] | float,
    max_arg: float = 700.0,
) -> npt.NDArray[np.float64]:
    """
    Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to
    avoid overflow and using numpy.expm1 for better precision near zero.

    Parameters:
    - x: input value(s)
    - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

    Returns:
    - np.ndarray: exp(x) - 1 computed safely
    """
    arr = np.asarray(x, dtype=float)
    clipped = np.clip(arr, -max_arg, max_arg)
    out = np.expm1(clipped)
    return cast(npt.NDArray[np.float64], out)

srh_recombination(n, p, temperature=float(DEFAULT_T), tau_n=1e-06, tau_p=1e-06, n1=None, p1=None)

Calculate the Shockley-Read-Hall (SRH) recombination rate.

Parameters:

Name Type Description Default
n float | NDArray[floating]

Electron concentration (cm^-3)

required
p float | NDArray[floating]

Hole concentration (cm^-3)

required
temperature float

Temperature in Kelvin

float(DEFAULT_T)
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06

Returns:

Type Description
float | NDArray[floating]

SRH recombination rate (cm^-3 s^-1).

Notes

Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default. Advanced users can override n1 and p1 to relax this assumption.

Source code in semiconductor_sim/models/recombination.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def srh_recombination(
    n: float | npt.NDArray[np.floating],
    p: float | npt.NDArray[np.floating],
    temperature: float = float(DEFAULT_T),
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
    n1: float | None = None,
    p1: float | None = None,
) -> float | npt.NDArray[np.floating]:
    """
    Calculate the Shockley-Read-Hall (SRH) recombination rate.

    Parameters:
        n: Electron concentration (cm^-3)
        p: Hole concentration (cm^-3)
        temperature: Temperature in Kelvin
        tau_n: Electron lifetime (s)
        tau_p: Hole lifetime (s)

    Returns:
        SRH recombination rate (cm^-3 s^-1).

    Notes:
        Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default.
        Advanced users can override `n1` and `p1` to relax this assumption.
    """
    n_i = 1.5e10 * (temperature / float(DEFAULT_T)) ** 1.5
    n1_val = n_i if n1 is None else n1
    p1_val = n_i if p1 is None else p1

    n_arr = np.asarray(n, dtype=float)
    p_arr = np.asarray(p, dtype=float)

    denominator = tau_p * (n_arr + n1_val) + tau_n * (p_arr + p1_val)
    R_SRH = (n_arr * p_arr - n_i**2) / denominator

    if np.isscalar(n) and np.isscalar(p):
        return float(np.asarray(R_SRH).item())
    return np.asarray(R_SRH, dtype=float)

use_headless_backend(preferred='Agg')

Switch Matplotlib backend to a non-interactive one if possible.

Parameters: - preferred: Backend name to use when switching (default: 'Agg').

Source code in semiconductor_sim/utils/plotting.py
10
11
12
13
14
15
16
17
18
def use_headless_backend(preferred: str = "Agg") -> None:
    """
    Switch Matplotlib backend to a non-interactive one if possible.

    Parameters:
    - preferred: Backend name to use when switching (default: 'Agg').
    """
    with suppress(Exception):
        matplotlib.use(preferred, force=True)

DEFAULT_T = 300 module-attribute

k_B = 1.381e-23 module-attribute

q = 1.602e-19 module-attribute

Device(area=0.0001, temperature=DEFAULT_T)

Bases: ABC

Abstract base class for semiconductor devices.

Provides common fields and establishes a standard API for IV characteristics. Subclasses must implement iv_characteristic.

Source code in semiconductor_sim/devices/base.py
19
20
21
22
23
24
25
def __init__(self, area: float = 1e-4, temperature: float = DEFAULT_T) -> None:
    if not np.isfinite(area) or area <= 0:
        raise ValueError("area must be a positive finite value (cm^2)")
    if not np.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be a positive finite value (K)")
    self.area = area
    self.temperature = temperature

iv_characteristic(voltage_array, n_conc=None, p_conc=None) abstractmethod

Compute current vs. voltage. Implementations must return a tuple where the first element is the current array, and optional subsequent arrays include model-specific outputs (e.g., recombination rates, emission).

Source code in semiconductor_sim/devices/base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Compute current vs. voltage. Implementations must return a tuple where the
    first element is the current array, and optional subsequent arrays include
    model-specific outputs (e.g., recombination rates, emission).
    """
    raise NotImplementedError

ZenerDiode(doping_p, doping_n, area=0.0001, zener_voltage=5.0, temperature=300, tau_n=1e-06, tau_p=1e-06)

Bases: Device

Initialize the Zener Diode.

Parameters:

Name Type Description Default
doping_p float

Acceptor concentration in p-region (cm^-3)

required
doping_n float

Donor concentration in n-region (cm^-3)

required
area float

Cross-sectional area of the diode (cm^2)

0.0001
zener_voltage float

Zener breakdown voltage (V)

5.0
temperature float

Temperature in Kelvin

300
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06
Source code in semiconductor_sim/devices/zener_diode.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(
    self,
    doping_p,
    doping_n,
    area=1e-4,
    zener_voltage=5.0,
    temperature=300,
    tau_n=1e-6,
    tau_p=1e-6,
):
    """
    Initialize the Zener Diode.

    Parameters:
        doping_p (float): Acceptor concentration in p-region (cm^-3)
        doping_n (float): Donor concentration in n-region (cm^-3)
        area (float): Cross-sectional area of the diode (cm^2)
        zener_voltage (float): Zener breakdown voltage (V)
        temperature (float): Temperature in Kelvin
        tau_n (float): Electron lifetime (s)
        tau_p (float): Hole lifetime (s)
    """
    super().__init__(area=area, temperature=temperature)
    self.doping_p = doping_p
    self.doping_n = doping_n
    self.zener_voltage = zener_voltage
    self.tau_n = tau_n
    self.tau_p = tau_p
    self.I_s = self.calculate_saturation_current()
    self.model = self.load_ml_model()

calculate_saturation_current()

Calculate the saturation current (I_s) considering temperature.

Source code in semiconductor_sim/devices/zener_diode.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def calculate_saturation_current(self):
    """
    Calculate the saturation current (I_s) considering temperature.
    """
    D_n = 25  # Electron diffusion coefficient (cm^2/s)
    D_p = 10  # Hole diffusion coefficient (cm^2/s)
    L_n = 5e-4  # Electron diffusion length (cm)
    L_p = 5e-4  # Hole diffusion length (cm)
    n_i = 1.5e10 * (self.temperature / DEFAULT_T) ** 1.5  # Intrinsic carrier concentration

    I_s = (
        q * self.area * n_i**2 * ((D_p / (L_p * self.doping_n)) + (D_n / (L_n * self.doping_p)))
    )
    return I_s

iv_characteristic(voltage_array, n_conc=None, p_conc=None)

Calculate the current for a given array of voltages, including SRH recombination.

Parameters:

Name Type Description Default
voltage_array NDArray[floating]

Array of voltage values (V)

required
n_conc float | NDArray[floating] | None

Electron concentration (cm^-3)

None
p_conc float | NDArray[floating] | None

Hole concentration (cm^-3)

None

Returns:

Name Type Description
current_array NDArray[floating]

Array of current values (A)

recombination_array NDArray[floating]

Array of recombination rates (cm^-3 s^-1)

Source code in semiconductor_sim/devices/zener_diode.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
    """
    Calculate the current for a given array of voltages, including SRH recombination.

    Parameters:
        voltage_array: Array of voltage values (V)
        n_conc: Electron concentration (cm^-3)
        p_conc: Hole concentration (cm^-3)

    Returns:
        current_array: Array of current values (A)
        recombination_array: Array of recombination rates (cm^-3 s^-1)
    """
    # Update Zener voltage prediction
    self.zener_voltage = self.predict_zener_voltage()

    V_T = k_B * self.temperature / q  # Thermal voltage
    I = self.I_s * safe_expm1(voltage_array / V_T)

    # Implement Zener breakdown
    I_breakdown = np.where(
        voltage_array >= self.zener_voltage,
        0.1 * self.I_s * (voltage_array - self.zener_voltage),
        0,
    )
    I += I_breakdown

    if n_conc is not None and p_conc is not None:
        R_SRH = srh_recombination(
            n_conc, p_conc, temperature=self.temperature, tau_n=self.tau_n, tau_p=self.tau_p
        )
        R_SRH = np.broadcast_to(R_SRH, np.shape(voltage_array))
    else:
        R_SRH = np.zeros_like(voltage_array)

    return np.asarray(I, dtype=float), np.asarray(R_SRH, dtype=float)

load_ml_model()

Load the pre-trained ML model for predicting Zener voltage.

Source code in semiconductor_sim/devices/zener_diode.py
65
66
67
68
69
70
71
72
73
74
75
76
77
def load_ml_model(self):
    """
    Load the pre-trained ML model for predicting Zener voltage.
    """
    model_path = os.path.join(
        os.path.dirname(__file__), '..', '..', 'models', 'zener_voltage_rf_model.pkl'
    )
    if os.path.exists(model_path):
        model = joblib.load(model_path)
        return model
    else:
        print("ML model for Zener voltage not found. Using default value.")
        return None

plot_iv_characteristic(voltage, current, recombination=None)

Plot the IV characteristics and optionally the recombination rate.

Parameters:

Name Type Description Default
voltage NDArray[floating]

Voltage values (V)

required
current NDArray[floating]

Current values (A)

required
recombination NDArray[floating] | None

Recombination rates (cm^-3 s^-1)

None
Source code in semiconductor_sim/devices/zener_diode.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def plot_iv_characteristic(
    self,
    voltage: npt.NDArray[np.floating],
    current: npt.NDArray[np.floating],
    recombination: npt.NDArray[np.floating] | None = None,
) -> None:
    """
    Plot the IV characteristics and optionally the recombination rate.

    Parameters:
        voltage: Voltage values (V)
        current: Current values (A)
        recombination: Recombination rates (cm^-3 s^-1)
    """
    use_headless_backend("Agg")
    apply_basic_style()
    fig, ax1 = plt.subplots(figsize=(8, 6))

    color = 'tab:blue'
    ax1.set_xlabel('Voltage (V)')
    ax1.set_ylabel('Current (A)', color=color)
    ax1.plot(voltage, current, color=color, label='IV Characteristic')
    ax1.tick_params(axis='y', labelcolor=color)
    ax1.grid(True)

    if recombination is not None:
        ax2 = ax1.twinx()
        color = 'tab:green'
        ax2.set_ylabel('Recombination Rate (cm$^{-3}$ s$^{-1}$)', color=color)
        ax2.plot(voltage, recombination, color=color, label='SRH Recombination')
        ax2.tick_params(axis='y', labelcolor=color)

    fig.tight_layout()
    plt.title('Zener Diode IV Characteristics')
    plt.show()

predict_zener_voltage()

Predict the Zener voltage using the ML model.

Returns:

Name Type Description
predicted_zener_voltage float

Predicted Zener voltage (V)

Source code in semiconductor_sim/devices/zener_diode.py
79
80
81
82
83
84
85
86
87
88
89
90
91
def predict_zener_voltage(self):
    """
    Predict the Zener voltage using the ML model.

    Returns:
        predicted_zener_voltage (float): Predicted Zener voltage (V)
    """
    if self.model:
        input_features = np.array([[self.doping_p, self.doping_n, self.temperature]])
        predicted_zener_voltage = self.model.predict(input_features)[0]
        return predicted_zener_voltage
    else:
        return self.zener_voltage

apply_basic_style()

Apply a minimal style to keep plots consistent across devices.

Source code in semiconductor_sim/utils/plotting.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def apply_basic_style() -> None:
    """Apply a minimal style to keep plots consistent across devices."""
    with suppress(Exception):
        import matplotlib.pyplot as plt

        plt.rcParams.update(
            {
                "axes.grid": True,
                "axes.titlesize": 12,
                "axes.labelsize": 10,
                "legend.fontsize": 9,
                "figure.figsize": (8, 6),
            }
        )

safe_expm1(x, max_arg=700.0)

Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to avoid overflow and using numpy.expm1 for better precision near zero.

Parameters: - x: input value(s) - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

Returns: - np.ndarray: exp(x) - 1 computed safely

Source code in semiconductor_sim/utils/numerics.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def safe_expm1(
    x: npt.NDArray[np.floating] | float,
    max_arg: float = 700.0,
) -> npt.NDArray[np.float64]:
    """
    Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to
    avoid overflow and using numpy.expm1 for better precision near zero.

    Parameters:
    - x: input value(s)
    - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

    Returns:
    - np.ndarray: exp(x) - 1 computed safely
    """
    arr = np.asarray(x, dtype=float)
    clipped = np.clip(arr, -max_arg, max_arg)
    out = np.expm1(clipped)
    return cast(npt.NDArray[np.float64], out)

srh_recombination(n, p, temperature=float(DEFAULT_T), tau_n=1e-06, tau_p=1e-06, n1=None, p1=None)

Calculate the Shockley-Read-Hall (SRH) recombination rate.

Parameters:

Name Type Description Default
n float | NDArray[floating]

Electron concentration (cm^-3)

required
p float | NDArray[floating]

Hole concentration (cm^-3)

required
temperature float

Temperature in Kelvin

float(DEFAULT_T)
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06

Returns:

Type Description
float | NDArray[floating]

SRH recombination rate (cm^-3 s^-1).

Notes

Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default. Advanced users can override n1 and p1 to relax this assumption.

Source code in semiconductor_sim/models/recombination.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def srh_recombination(
    n: float | npt.NDArray[np.floating],
    p: float | npt.NDArray[np.floating],
    temperature: float = float(DEFAULT_T),
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
    n1: float | None = None,
    p1: float | None = None,
) -> float | npt.NDArray[np.floating]:
    """
    Calculate the Shockley-Read-Hall (SRH) recombination rate.

    Parameters:
        n: Electron concentration (cm^-3)
        p: Hole concentration (cm^-3)
        temperature: Temperature in Kelvin
        tau_n: Electron lifetime (s)
        tau_p: Hole lifetime (s)

    Returns:
        SRH recombination rate (cm^-3 s^-1).

    Notes:
        Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default.
        Advanced users can override `n1` and `p1` to relax this assumption.
    """
    n_i = 1.5e10 * (temperature / float(DEFAULT_T)) ** 1.5
    n1_val = n_i if n1 is None else n1
    p1_val = n_i if p1 is None else p1

    n_arr = np.asarray(n, dtype=float)
    p_arr = np.asarray(p, dtype=float)

    denominator = tau_p * (n_arr + n1_val) + tau_n * (p_arr + p1_val)
    R_SRH = (n_arr * p_arr - n_i**2) / denominator

    if np.isscalar(n) and np.isscalar(p):
        return float(np.asarray(R_SRH).item())
    return np.asarray(R_SRH, dtype=float)

use_headless_backend(preferred='Agg')

Switch Matplotlib backend to a non-interactive one if possible.

Parameters: - preferred: Backend name to use when switching (default: 'Agg').

Source code in semiconductor_sim/utils/plotting.py
10
11
12
13
14
15
16
17
18
def use_headless_backend(preferred: str = "Agg") -> None:
    """
    Switch Matplotlib backend to a non-interactive one if possible.

    Parameters:
    - preferred: Backend name to use when switching (default: 'Agg').
    """
    with suppress(Exception):
        matplotlib.use(preferred, force=True)

MOS capacitor device model.

DEFAULT_T = 300 module-attribute

epsilon_0 = 8.854e-14 module-attribute

k_B = 1.381e-23 module-attribute

q = 1.602e-19 module-attribute

Device(area=0.0001, temperature=DEFAULT_T)

Bases: ABC

Abstract base class for semiconductor devices.

Provides common fields and establishes a standard API for IV characteristics. Subclasses must implement iv_characteristic.

Source code in semiconductor_sim/devices/base.py
19
20
21
22
23
24
25
def __init__(self, area: float = 1e-4, temperature: float = DEFAULT_T) -> None:
    if not np.isfinite(area) or area <= 0:
        raise ValueError("area must be a positive finite value (cm^2)")
    if not np.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be a positive finite value (K)")
    self.area = area
    self.temperature = temperature

iv_characteristic(voltage_array, n_conc=None, p_conc=None) abstractmethod

Compute current vs. voltage. Implementations must return a tuple where the first element is the current array, and optional subsequent arrays include model-specific outputs (e.g., recombination rates, emission).

Source code in semiconductor_sim/devices/base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Compute current vs. voltage. Implementations must return a tuple where the
    first element is the current array, and optional subsequent arrays include
    model-specific outputs (e.g., recombination rates, emission).
    """
    raise NotImplementedError

MOSCapacitor(doping_p, oxide_thickness=1e-06, oxide_permittivity=3.45, area=0.0001, temperature=DEFAULT_T, tau_n=1e-06, tau_p=1e-06)

Bases: Device

Initialize the MOS Capacitor.

Parameters:

Name Type Description Default
doping_p float

Acceptor concentration in p-region (cm^-3)

required
oxide_thickness float

Oxide thickness (cm)

1e-06
oxide_permittivity float

Relative permittivity of the oxide

3.45
area float

Cross-sectional area of the capacitor (cm^2)

0.0001
temperature float

Temperature in Kelvin

DEFAULT_T
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06
Source code in semiconductor_sim/devices/mos_capacitor.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def __init__(
    self,
    doping_p: float,
    oxide_thickness: float = 1e-6,
    oxide_permittivity: float = 3.45,
    area: float = 1e-4,
    temperature: float = DEFAULT_T,
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
):
    """
    Initialize the MOS Capacitor.

    Parameters:
        doping_p (float): Acceptor concentration in p-region (cm^-3)
        oxide_thickness (float): Oxide thickness (cm)
        oxide_permittivity (float): Relative permittivity of the oxide
        area (float): Cross-sectional area of the capacitor (cm^2)
        temperature (float): Temperature in Kelvin
        tau_n (float): Electron lifetime (s)
        tau_p (float): Hole lifetime (s)
    """
    super().__init__(area=area, temperature=temperature)
    self.doping_p = doping_p
    self.oxide_thickness = oxide_thickness
    self.oxide_permittivity = oxide_permittivity
    self.tau_n = tau_n
    self.tau_p = tau_p
    self.C_ox = self.calculate_oxide_capacitance()

calculate_oxide_capacitance()

Calculate the oxide capacitance (C_ox).

Source code in semiconductor_sim/devices/mos_capacitor.py
45
46
47
48
49
def calculate_oxide_capacitance(self) -> float:
    """Calculate the oxide capacitance (C_ox)."""
    epsilon_ox = self.oxide_permittivity * epsilon_0  # F/cm
    C_ox = epsilon_ox * self.area / self.oxide_thickness  # F
    return C_ox

capacitance(applied_voltage)

Calculate the capacitance as a function of applied voltage.

Source code in semiconductor_sim/devices/mos_capacitor.py
62
63
64
65
66
67
68
69
70
71
72
73
def capacitance(self, applied_voltage: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
    """Calculate the capacitance as a function of applied voltage."""
    # In accumulation, capacitance is C_ox
    # In depletion, capacitance decreases with increasing reverse bias
    # In inversion, capacitance approaches C_ox again

    W = self.depletion_width(applied_voltage)
    C_depl = epsilon_0 * self.oxide_permittivity * self.area / W

    # Simplified model: capacitance transitions from C_ox to C_depl
    C = np.where(applied_voltage < 0, C_depl, self.C_ox)
    return np.asarray(C, dtype=float)

depletion_width(applied_voltage)

Calculate the depletion width for a given applied voltage.

Source code in semiconductor_sim/devices/mos_capacitor.py
51
52
53
54
55
56
57
58
59
60
def depletion_width(
    self, applied_voltage: npt.NDArray[np.floating]
) -> npt.NDArray[np.floating]:
    """Calculate the depletion width for a given applied voltage."""
    # Built-in potential (simplified)
    V_bi = 0.7  # Volts, adjust as needed
    # Use effective reverse bias magnitude: positive gate voltage increases depletion
    V = V_bi + np.maximum(applied_voltage, 0)
    W = np.sqrt((2 * epsilon_0 * self.oxide_permittivity * V) / (q * self.doping_p))
    return np.asarray(W, dtype=float)

iv_characteristic(voltage_array, n_conc=None, p_conc=None)

Calculate current for voltage_array; optionally compute SRH recombination.

Source code in semiconductor_sim/devices/mos_capacitor.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
    """Calculate current for `voltage_array`; optionally compute SRH recombination."""
    V_T = k_B * self.temperature / q  # Thermal voltage
    I = self.C_ox * (voltage_array) / V_T  # Simplified current model

    if n_conc is not None and p_conc is not None:
        R_SRH_val = srh_recombination(
            n_conc, p_conc, temperature=self.temperature, tau_n=self.tau_n, tau_p=self.tau_p
        )
        R_SRH = np.broadcast_to(np.asarray(R_SRH_val, dtype=float), np.shape(voltage_array))
    else:
        R_SRH = np.zeros_like(voltage_array)

    return np.asarray(I, dtype=float), np.asarray(R_SRH, dtype=float)

plot_capacitance_vs_voltage(voltage)

Plot the capacitance-voltage (C-V) characteristics.

Computes capacitance internally from the provided voltage array.

Source code in semiconductor_sim/devices/mos_capacitor.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def plot_capacitance_vs_voltage(self, voltage: npt.NDArray[np.floating]) -> None:
    """Plot the capacitance-voltage (C-V) characteristics.

    Computes capacitance internally from the provided voltage array.
    """
    capacitance = self.capacitance(voltage)
    use_headless_backend("Agg")
    apply_basic_style()
    plt.figure(figsize=(8, 6))
    plt.plot(voltage, capacitance, label='C-V Characteristic')
    plt.title('MOS Capacitor C-V Characteristics')
    plt.xlabel('Gate Voltage (V)')
    plt.ylabel('Capacitance (F)')
    plt.grid(True)
    plt.legend()
    plt.show()

plot_iv_characteristic(voltage, current, recombination=None)

Plot the IV characteristics and optionally the recombination rate.

Source code in semiconductor_sim/devices/mos_capacitor.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def plot_iv_characteristic(
    self,
    voltage: npt.NDArray[np.floating],
    current: npt.NDArray[np.floating],
    recombination: npt.NDArray[np.floating] | None = None,
) -> None:
    """Plot the IV characteristics and optionally the recombination rate."""
    use_headless_backend("Agg")
    apply_basic_style()
    fig, ax1 = plt.subplots(figsize=(8, 6))

    color = 'tab:blue'
    ax1.set_xlabel('Voltage (V)')
    ax1.set_ylabel('Current (A)', color=color)
    ax1.plot(voltage, current, color=color, label='IV Characteristic')
    ax1.tick_params(axis='y', labelcolor=color)
    ax1.grid(True)

    if recombination is not None:
        ax2 = ax1.twinx()
        color = 'tab:green'
        ax2.set_ylabel('Recombination Rate (cm$^{-3}$ s$^{-1}$)', color=color)
        ax2.plot(voltage, recombination, color=color, label='SRH Recombination')
        ax2.tick_params(axis='y', labelcolor=color)

    fig.tight_layout()
    plt.title('MOS Capacitor IV Characteristics')
    plt.show()

apply_basic_style()

Apply a minimal style to keep plots consistent across devices.

Source code in semiconductor_sim/utils/plotting.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def apply_basic_style() -> None:
    """Apply a minimal style to keep plots consistent across devices."""
    with suppress(Exception):
        import matplotlib.pyplot as plt

        plt.rcParams.update(
            {
                "axes.grid": True,
                "axes.titlesize": 12,
                "axes.labelsize": 10,
                "legend.fontsize": 9,
                "figure.figsize": (8, 6),
            }
        )

srh_recombination(n, p, temperature=float(DEFAULT_T), tau_n=1e-06, tau_p=1e-06, n1=None, p1=None)

Calculate the Shockley-Read-Hall (SRH) recombination rate.

Parameters:

Name Type Description Default
n float | NDArray[floating]

Electron concentration (cm^-3)

required
p float | NDArray[floating]

Hole concentration (cm^-3)

required
temperature float

Temperature in Kelvin

float(DEFAULT_T)
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06

Returns:

Type Description
float | NDArray[floating]

SRH recombination rate (cm^-3 s^-1).

Notes

Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default. Advanced users can override n1 and p1 to relax this assumption.

Source code in semiconductor_sim/models/recombination.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def srh_recombination(
    n: float | npt.NDArray[np.floating],
    p: float | npt.NDArray[np.floating],
    temperature: float = float(DEFAULT_T),
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
    n1: float | None = None,
    p1: float | None = None,
) -> float | npt.NDArray[np.floating]:
    """
    Calculate the Shockley-Read-Hall (SRH) recombination rate.

    Parameters:
        n: Electron concentration (cm^-3)
        p: Hole concentration (cm^-3)
        temperature: Temperature in Kelvin
        tau_n: Electron lifetime (s)
        tau_p: Hole lifetime (s)

    Returns:
        SRH recombination rate (cm^-3 s^-1).

    Notes:
        Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default.
        Advanced users can override `n1` and `p1` to relax this assumption.
    """
    n_i = 1.5e10 * (temperature / float(DEFAULT_T)) ** 1.5
    n1_val = n_i if n1 is None else n1
    p1_val = n_i if p1 is None else p1

    n_arr = np.asarray(n, dtype=float)
    p_arr = np.asarray(p, dtype=float)

    denominator = tau_p * (n_arr + n1_val) + tau_n * (p_arr + p1_val)
    R_SRH = (n_arr * p_arr - n_i**2) / denominator

    if np.isscalar(n) and np.isscalar(p):
        return float(np.asarray(R_SRH).item())
    return np.asarray(R_SRH, dtype=float)

use_headless_backend(preferred='Agg')

Switch Matplotlib backend to a non-interactive one if possible.

Parameters: - preferred: Backend name to use when switching (default: 'Agg').

Source code in semiconductor_sim/utils/plotting.py
10
11
12
13
14
15
16
17
18
def use_headless_backend(preferred: str = "Agg") -> None:
    """
    Switch Matplotlib backend to a non-interactive one if possible.

    Parameters:
    - preferred: Backend name to use when switching (default: 'Agg').
    """
    with suppress(Exception):
        matplotlib.use(preferred, force=True)

Photodiode device model (teaching-simple).

Modeled as an illuminated diode: I(V) = -I_ph + I_s * (exp(V / V_T) - 1).

Assumptions: - Constant responsivity over spectrum; photocurrent proportional to irradiance. - Uses same dark-saturation current form as PN junction device. - Optional material to compute intrinsic carrier density dependence.

DEFAULT_T = 300 module-attribute

k_B = 1.381e-23 module-attribute

q = 1.602e-19 module-attribute

Device(area=0.0001, temperature=DEFAULT_T)

Bases: ABC

Abstract base class for semiconductor devices.

Provides common fields and establishes a standard API for IV characteristics. Subclasses must implement iv_characteristic.

Source code in semiconductor_sim/devices/base.py
19
20
21
22
23
24
25
def __init__(self, area: float = 1e-4, temperature: float = DEFAULT_T) -> None:
    if not np.isfinite(area) or area <= 0:
        raise ValueError("area must be a positive finite value (cm^2)")
    if not np.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be a positive finite value (K)")
    self.area = area
    self.temperature = temperature

iv_characteristic(voltage_array, n_conc=None, p_conc=None) abstractmethod

Compute current vs. voltage. Implementations must return a tuple where the first element is the current array, and optional subsequent arrays include model-specific outputs (e.g., recombination rates, emission).

Source code in semiconductor_sim/devices/base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Compute current vs. voltage. Implementations must return a tuple where the
    first element is the current array, and optional subsequent arrays include
    model-specific outputs (e.g., recombination rates, emission).
    """
    raise NotImplementedError

Material(name, symbol, Eg0_eV, varshni_alpha_eV_per_K, varshni_beta_K, Nc_prefactor_cm3, Nv_prefactor_cm3) dataclass

Photodiode(doping_p, doping_n, area=0.0001, irradiance_W_per_cm2=0.001, responsivity_A_per_W=0.5, temperature=DEFAULT_T, material=None)

Bases: Device

Initialize a photodiode.

Parameters:

Name Type Description Default
doping_p float

Acceptor concentration in p-region (cm^-3)

required
doping_n float

Donor concentration in n-region (cm^-3)

required
area float

Active area of the photodiode (cm^2)

0.0001
irradiance_W_per_cm2 float

Incident optical power density (W/cm^2)

0.001
responsivity_A_per_W float

Responsivity (A/W)

0.5
temperature float

Temperature in Kelvin

DEFAULT_T
material Material | None

Optional material for intrinsic density model

None
Source code in semiconductor_sim/devices/photodiode.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def __init__(
    self,
    doping_p: float,
    doping_n: float,
    area: float = 1e-4,
    irradiance_W_per_cm2: float = 1e-3,
    responsivity_A_per_W: float = 0.5,
    temperature: float = DEFAULT_T,
    material: Material | None = None,
) -> None:
    """Initialize a photodiode.

    Parameters:
        doping_p: Acceptor concentration in p-region (cm^-3)
        doping_n: Donor concentration in n-region (cm^-3)
        area: Active area of the photodiode (cm^2)
        irradiance_W_per_cm2: Incident optical power density (W/cm^2)
        responsivity_A_per_W: Responsivity (A/W)
        temperature: Temperature in Kelvin
        material: Optional material for intrinsic density model
    """
    super().__init__(area=area, temperature=temperature)
    self.doping_p = float(doping_p)
    self.doping_n = float(doping_n)
    self.irradiance_W_per_cm2 = float(irradiance_W_per_cm2)
    self.responsivity_A_per_W = float(responsivity_A_per_W)
    self.material = material

iv_characteristic(voltage_array, n_conc=None, p_conc=None)

Return the illuminated I–V curve as a tuple with current array.

Returns:

Type Description
tuple[NDArray[floating], ...]

(current_array,)

Source code in semiconductor_sim/devices/photodiode.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """Return the illuminated I–V curve as a tuple with current array.

    Returns:
        (current_array,)
    """
    V_T = k_B * self.temperature / q
    I_s = self._dark_saturation_current()
    I_ph = self._photocurrent()
    I = -I_ph + I_s * safe_expm1(voltage_array / V_T)
    return (np.asarray(I, dtype=float),)

safe_expm1(x, max_arg=700.0)

Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to avoid overflow and using numpy.expm1 for better precision near zero.

Parameters: - x: input value(s) - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

Returns: - np.ndarray: exp(x) - 1 computed safely

Source code in semiconductor_sim/utils/numerics.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def safe_expm1(
    x: npt.NDArray[np.floating] | float,
    max_arg: float = 700.0,
) -> npt.NDArray[np.float64]:
    """
    Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to
    avoid overflow and using numpy.expm1 for better precision near zero.

    Parameters:
    - x: input value(s)
    - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

    Returns:
    - np.ndarray: exp(x) - 1 computed safely
    """
    arr = np.asarray(x, dtype=float)
    clipped = np.clip(arr, -max_arg, max_arg)
    out = np.expm1(clipped)
    return cast(npt.NDArray[np.float64], out)

BARRIER_MAX_EV = 2.0 module-attribute

BARRIER_MIN_EV = 0.1 module-attribute

IDEALITY_MAX = 2.0 module-attribute

IDEALITY_MIN = 1.0 module-attribute

k_B = 1.381e-23 module-attribute

q = 1.602e-19 module-attribute

Device(area=0.0001, temperature=DEFAULT_T)

Bases: ABC

Abstract base class for semiconductor devices.

Provides common fields and establishes a standard API for IV characteristics. Subclasses must implement iv_characteristic.

Source code in semiconductor_sim/devices/base.py
19
20
21
22
23
24
25
def __init__(self, area: float = 1e-4, temperature: float = DEFAULT_T) -> None:
    if not np.isfinite(area) or area <= 0:
        raise ValueError("area must be a positive finite value (cm^2)")
    if not np.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be a positive finite value (K)")
    self.area = area
    self.temperature = temperature

iv_characteristic(voltage_array, n_conc=None, p_conc=None) abstractmethod

Compute current vs. voltage. Implementations must return a tuple where the first element is the current array, and optional subsequent arrays include model-specific outputs (e.g., recombination rates, emission).

Source code in semiconductor_sim/devices/base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def iv_characteristic(
    self,
    voltage_array: npt.NDArray[np.floating],
    n_conc: float | npt.NDArray[np.floating] | None = None,
    p_conc: float | npt.NDArray[np.floating] | None = None,
) -> tuple[npt.NDArray[np.floating], ...]:
    """
    Compute current vs. voltage. Implementations must return a tuple where the
    first element is the current array, and optional subsequent arrays include
    model-specific outputs (e.g., recombination rates, emission).
    """
    raise NotImplementedError

SchottkyDiode(barrier_height_eV=0.7, ideality=1.1, *, area=0.0001, temperature=300.0, A_star=120.0, series_resistance_ohm=None)

Bases: Device

Teaching-simple Schottky diode using thermionic emission.

I = A * A** * T^2 * exp(-qΦ_B/kT) * [exp(qV/nkT) - 1]

Parameters - barrier_height_eV: Φ_B in eV - ideality: n (default 1.1) - area: junction area in cm^2 (default 1e-4) - temperature: K - A_star: effective Richardson constant (A/cm^2/K^2), default 120 for Si

Source code in semiconductor_sim/devices/schottky.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(
    self,
    barrier_height_eV: float = 0.7,
    ideality: float = 1.1,
    *,
    area: float = 1e-4,
    temperature: float = 300.0,
    A_star: float = 120.0,
    series_resistance_ohm: float | None = None,
) -> None:
    super().__init__(area=area, temperature=temperature)
    if not (BARRIER_MIN_EV <= barrier_height_eV <= BARRIER_MAX_EV):
        raise ValueError(f"barrier_height_eV out of range [{BARRIER_MIN_EV}, {BARRIER_MAX_EV}]")
    if not (IDEALITY_MIN <= ideality <= IDEALITY_MAX):
        raise ValueError(f"ideality should be in [{IDEALITY_MIN}, {IDEALITY_MAX}]")
    self.barrier_height_eV = barrier_height_eV
    self.ideality = ideality
    self.A_star = A_star
    self.series_resistance_ohm = (
        series_resistance_ohm if series_resistance_ohm and series_resistance_ohm > 0 else None
    )

Models

Recombination models.

DEFAULT_T = 300 module-attribute

srh_recombination(n, p, temperature=float(DEFAULT_T), tau_n=1e-06, tau_p=1e-06, n1=None, p1=None)

Calculate the Shockley-Read-Hall (SRH) recombination rate.

Parameters:

Name Type Description Default
n float | NDArray[floating]

Electron concentration (cm^-3)

required
p float | NDArray[floating]

Hole concentration (cm^-3)

required
temperature float

Temperature in Kelvin

float(DEFAULT_T)
tau_n float

Electron lifetime (s)

1e-06
tau_p float

Hole lifetime (s)

1e-06

Returns:

Type Description
float | NDArray[floating]

SRH recombination rate (cm^-3 s^-1).

Notes

Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default. Advanced users can override n1 and p1 to relax this assumption.

Source code in semiconductor_sim/models/recombination.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def srh_recombination(
    n: float | npt.NDArray[np.floating],
    p: float | npt.NDArray[np.floating],
    temperature: float = float(DEFAULT_T),
    tau_n: float = 1e-6,
    tau_p: float = 1e-6,
    n1: float | None = None,
    p1: float | None = None,
) -> float | npt.NDArray[np.floating]:
    """
    Calculate the Shockley-Read-Hall (SRH) recombination rate.

    Parameters:
        n: Electron concentration (cm^-3)
        p: Hole concentration (cm^-3)
        temperature: Temperature in Kelvin
        tau_n: Electron lifetime (s)
        tau_p: Hole lifetime (s)

    Returns:
        SRH recombination rate (cm^-3 s^-1).

    Notes:
        Uses a simplified SRH form assuming mid-gap trap with n1 ≈ p1 ≈ n_i by default.
        Advanced users can override `n1` and `p1` to relax this assumption.
    """
    n_i = 1.5e10 * (temperature / float(DEFAULT_T)) ** 1.5
    n1_val = n_i if n1 is None else n1
    p1_val = n_i if p1 is None else p1

    n_arr = np.asarray(n, dtype=float)
    p_arr = np.asarray(p, dtype=float)

    denominator = tau_p * (n_arr + n1_val) + tau_n * (p_arr + p1_val)
    R_SRH = (n_arr * p_arr - n_i**2) / denominator

    if np.isscalar(n) and np.isscalar(p):
        return float(np.asarray(R_SRH).item())
    return np.asarray(R_SRH, dtype=float)

Radiative recombination model.

DEFAULT_T = 300 module-attribute

radiative_recombination(n, p, B=1e-10, temperature=DEFAULT_T)

Compute the radiative recombination rate.

Parameters: - n: Electron concentration (cm^-3) - p: Hole concentration (cm^-3) - B: Radiative recombination coefficient (cm^3/s) - temperature: Temperature in Kelvin (unused in simplified model)

Returns: - Radiative recombination rate (cm^-3 s^-1)

Notes: - Uses a simplified relation R = B * (n p - n_i^2), with n_i = 1.5e10 cm^-3. - Clamps negative values to zero for physical plausibility.

Source code in semiconductor_sim/models/radiative_recombination.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def radiative_recombination(
    n: float | npt.NDArray[np.floating],
    p: float | npt.NDArray[np.floating],
    B: float = 1e-10,
    temperature: float = DEFAULT_T,
) -> float | npt.NDArray[np.floating]:
    """Compute the radiative recombination rate.

    Parameters:
    - n: Electron concentration (cm^-3)
    - p: Hole concentration (cm^-3)
    - B: Radiative recombination coefficient (cm^3/s)
    - temperature: Temperature in Kelvin (unused in simplified model)

    Returns:
    - Radiative recombination rate (cm^-3 s^-1)

    Notes:
    - Uses a simplified relation R = B * (n p - n_i^2), with n_i = 1.5e10 cm^-3.
    - Clamps negative values to zero for physical plausibility.
    """
    ni_sq = (1.5e10) ** 2
    R = B * (n * p - ni_sq)
    if isinstance(R, np.ndarray):
        return np.maximum(R, 0)
    return max(float(R), 0.0)

DEFAULT_T = 300 module-attribute

auger_recombination(n, p, C=1e-31, temperature=DEFAULT_T)

Calculate the Auger Recombination rate.

Parameters: n (float or array): Electron concentration (cm^-3) p (float or array): Hole concentration (cm^-3) C (float): Auger recombination coefficient (cm^6/s) temperature (float): Temperature in Kelvin

Returns:

Name Type Description
R_auger float or array

Auger recombination rate (cm^-3 s^-1)

Source code in semiconductor_sim/models/auger_recombination.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def auger_recombination(
    n: float | npt.NDArray[np.floating],
    p: float | npt.NDArray[np.floating],
    C: float = 1e-31,
    temperature: float = DEFAULT_T,
) -> float | npt.NDArray[np.floating]:
    """
    Calculate the Auger Recombination rate.

    Parameters:
    n (float or array): Electron concentration (cm^-3)
    p (float or array): Hole concentration (cm^-3)
        C (float): Auger recombination coefficient (cm^6/s)
        temperature (float): Temperature in Kelvin

    Returns:
        R_auger (float or array): Auger recombination rate (cm^-3 s^-1)
    """
    R_auger = C * (n**2 * p + p**2 * n)
    return R_auger

temperature_dependent_bandgap(T, E_g0=1.12, alpha=0.000473, beta=636)

temperature_dependent_bandgap(T: float, E_g0: float = ..., alpha: float = ..., beta: float = ...) -> float
temperature_dependent_bandgap(T: NDArray[np.floating], E_g0: float = ..., alpha: float = ..., beta: float = ...) -> NDArray[np.floating]

Calculate the temperature-dependent bandgap energy using the Varshni equation.

Parameters:

Name Type Description Default
T float or ndarray

Temperature in Kelvin

required
E_g0 float

Bandgap energy at 0 K (eV)

1.12
alpha float

Varshni's alpha parameter (eV/K)

0.000473
beta float

Varshni's beta parameter (K)

636

Returns:

Name Type Description
E_g float or ndarray

Bandgap energy at temperature T (eV)

Source code in semiconductor_sim/models/bandgap.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def temperature_dependent_bandgap(
    T: float | NDArray[np.floating], E_g0: float = 1.12, alpha: float = 4.73e-4, beta: float = 636
) -> float | NDArray[np.floating]:
    """
    Calculate the temperature-dependent bandgap energy using the Varshni equation.

    Parameters:
        T (float or np.ndarray): Temperature in Kelvin
        E_g0 (float): Bandgap energy at 0 K (eV)
        alpha (float): Varshni's alpha parameter (eV/K)
        beta (float): Varshni's beta parameter (K)

    Returns:
        E_g (float or np.ndarray): Bandgap energy at temperature T (eV)
    """
    T_arr = np.asarray(T)
    E_g_arr = E_g0 - (alpha * T_arr**2) / (T_arr + beta)
    if E_g_arr.ndim == 0:
        return float(E_g_arr)
    return E_g_arr

high_frequency_capacitance(C_dc, f, R=1000)

Calculate the high-frequency capacitance using the voltage-dependent capacitance.

C_dc (float or array): DC capacitance (F) f (float): Frequency (Hz) R (float): Resistance (Ohms), default 1000 Ohms

Returns:

Name Type Description
C_ac float or array

AC capacitance (F)

Source code in semiconductor_sim/models/high_frequency.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def high_frequency_capacitance(
    C_dc: float | npt.NDArray[np.floating], f: float, R: float = 1000
) -> float | npt.NDArray[np.floating]:
    """
    Calculate the high-frequency capacitance using the voltage-dependent capacitance.

    Parameters:
    C_dc (float or array): DC capacitance (F)
        f (float): Frequency (Hz)
        R (float): Resistance (Ohms), default 1000 Ohms

    Returns:
        C_ac (float or array): AC capacitance (F)
    """
    omega = 2 * np.pi * f
    C_ac = C_dc / np.sqrt(1 + (omega * R * C_dc) ** 2)
    return C_ac

_SI_ELECTRON = dict(mu_min=92.0, mu_0=1414.0, N_ref=1.3e+17, alpha=0.91) module-attribute

_SI_HOLE = dict(mu_min=54.3, mu_0=470.0, N_ref=2.35e+17, alpha=0.88) module-attribute

_ct_model(N, mu_min, mu_0, N_ref, alpha, T=None, T_ref=300.0, temp_exp=0.0)

Source code in semiconductor_sim/models/mobility.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def _ct_model(
    N: float | NDArray[np.floating],
    mu_min: float,
    mu_0: float,
    N_ref: float,
    alpha: float,
    T: float | None = None,
    T_ref: float = 300.0,
    temp_exp: float = 0.0,
) -> float | NDArray[np.floating]:
    N_arr = np.asarray(N, dtype=float)
    mu = mu_min + (mu_0 - mu_min) / (1.0 + (N_arr / N_ref) ** alpha)
    if T is not None and temp_exp != 0.0:
        mu = mu * (np.asarray(T, dtype=float) / T_ref) ** (-temp_exp)
    return mu if isinstance(N, np.ndarray) else float(mu)

mu_n(doping_cm3, T=None, material='Si')

Source code in semiconductor_sim/models/mobility.py
31
32
33
34
35
36
37
38
39
def mu_n(
    doping_cm3: float | NDArray[np.floating],
    T: float | None = None,
    material: str = "Si",
) -> float | NDArray[np.floating]:
    if material != "Si":
        # For now, reuse Si as a placeholder for other materials.
        pass
    return _ct_model(doping_cm3, **_SI_ELECTRON, T=T, temp_exp=0.5)

mu_p(doping_cm3, T=None, material='Si')

Source code in semiconductor_sim/models/mobility.py
42
43
44
45
46
47
48
49
def mu_p(
    doping_cm3: float | NDArray[np.floating],
    T: float | None = None,
    material: str = "Si",
) -> float | NDArray[np.floating]:
    if material != "Si":
        pass
    return _ct_model(doping_cm3, **_SI_HOLE, T=T, temp_exp=0.7)

srh_rate(n, p, n_i, tau_n, tau_p)

Source code in semiconductor_sim/models/srh.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def srh_rate(
    n: float | NDArray[np.floating],
    p: float | NDArray[np.floating],
    n_i: float,
    tau_n: float,
    tau_p: float,
) -> NDArray[np.floating] | float:
    n_arr = np.asarray(n, dtype=float)
    p_arr = np.asarray(p, dtype=float)
    denom = tau_p * (n_arr + n_i) + tau_n * (p_arr + n_i)
    # Recombination rate R = (np - n_i^2) / denom
    R = (n_arr * p_arr - n_i**2) / denom
    return R if isinstance(n, np.ndarray) or isinstance(p, np.ndarray) else float(R)

Materials

__all__ = ['Material', 'get_material', 'list_materials', 'materials'] module-attribute

materials = {'Si': Material(name='Silicon', symbol='Si', Eg0_eV=1.17, varshni_alpha_eV_per_K=0.000473, varshni_beta_K=636.0, Nc_prefactor_cm3=6200000000000000.0, Nv_prefactor_cm3=3500000000000000.0), 'Ge': Material(name='Germanium', symbol='Ge', Eg0_eV=0.742, varshni_alpha_eV_per_K=0.00048, varshni_beta_K=235.0, Nc_prefactor_cm3=1980000000000000.0, Nv_prefactor_cm3=960000000000000.0), 'GaAs': Material(name='Gallium Arsenide', symbol='GaAs', Eg0_eV=1.519, varshni_alpha_eV_per_K=0.0005405, varshni_beta_K=204.0, Nc_prefactor_cm3=(4.7e+17 / 300.0 ** 1.5), Nv_prefactor_cm3=(9e+18 / 300.0 ** 1.5))} module-attribute

Material(name, symbol, Eg0_eV, varshni_alpha_eV_per_K, varshni_beta_K, Nc_prefactor_cm3, Nv_prefactor_cm3) dataclass

get_material(key)

Source code in semiconductor_sim/materials/registry.py
107
108
109
110
111
def get_material(key: str) -> Material:
    m = materials.get(key)
    if m is None:
        raise KeyError(f"Unknown material key: {key}. Available: {', '.join(materials)}")
    return m

list_materials()

Source code in semiconductor_sim/materials/registry.py
114
115
def list_materials() -> Iterable[str]:
    return materials.keys()

Utils

DEFAULT_T = 300 module-attribute

epsilon_0 = 8.854e-14 module-attribute

k_B = 1.381e-23 module-attribute

q = 1.602e-19 module-attribute

safe_expm1(x, max_arg=700.0)

Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to avoid overflow and using numpy.expm1 for better precision near zero.

Parameters: - x: input value(s) - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

Returns: - np.ndarray: exp(x) - 1 computed safely

Source code in semiconductor_sim/utils/numerics.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def safe_expm1(
    x: npt.NDArray[np.floating] | float,
    max_arg: float = 700.0,
) -> npt.NDArray[np.float64]:
    """
    Compute exp(x) - 1 safely for arrays or scalars by clipping the argument to
    avoid overflow and using numpy.expm1 for better precision near zero.

    Parameters:
    - x: input value(s)
    - max_arg: maximum absolute argument allowed before clipping (float64 ~709)

    Returns:
    - np.ndarray: exp(x) - 1 computed safely
    """
    arr = np.asarray(x, dtype=float)
    clipped = np.clip(arr, -max_arg, max_arg)
    out = np.expm1(clipped)
    return cast(npt.NDArray[np.float64], out)