Table of Contents

1  General Information

2  Defined constants and temperature dependencies

2.1  Bandgap $E_G$

2.2  Effectvie masses $m_n$ $m_p$

2.3  Effectvie density of states $N_c$ $N_v$

3  The Bulk model (Boltzmann)

3.1  Parameters

3.2  Computation of charge carries

3.3  Computation of Fermi level

3.4  Calculation of charge carrier in a doped semiconductor

4  The Bulk model (Fermi-Dirac)

5  The QV model (Boltzmann)

5.1  Parameters

5.2  Metal-semiconductor work function $\phi_{ms}$

5.3  The surface, acceptor and bulk potential

5.4  The electric field at the surface $E_{f,s}$

5.5  The potential loop

5.6  Computing the mobile channel charge $Q_m$

6  The QV model (Fermi Dirac)

7  The IV model (Boltzmann)

7.1  Parameters

7.2  The channel voltage $V_{ch}$

7.3  Effective mobility $\mu_{eff}$ as a function of the gate voltage

7.4  The drain to source current

```python
%load_ext autoreload
%autoreload 1
from CryMOS.constants import *
from CryMOS.QV import *
from CryMOS.plots import *
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
matplotlib.style.use('seaborn-notebook');
```

# CryMOS model documentation

```python
%load_ext autoreload
%autoreload 1
%matplotlib notebook
%aimport CryMOS.Bulk
from CryMOS.Bulk import *
from CryMOS.constants import *
from CryMOS.QV import *
from CryMOS.plots import *
from CryMOS.IV import *
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
matplotlib.style.use('seaborn-notebook')
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

## General Information
With the pm-mos-model current voltage characteristics of NMOS transistors
down to cryogenic temperatures can be computed. The model relies on the
charge sheet and fully depletion approximations \cite{TSIVIDIS} \cite{BREWS}. The default parameters
of this model (see chapter 2) refer to silicon as the bulk material of the
transistor. With the model also charge carrier concentrations and the Fermi energy of a bulk semiconductor can be computed.

## Defined constants and temperature dependencies

Constants and the temperature dependency of the bandgap and effective masses are defined in "constants.py".
### Bandgap $E_G$
The bandgap (as a function of temperature) is computed by an empirical
relation by Varshni \cite{VARSHNI}:
\begin{equation}
\label{eq:varshni}
E_g(T) = E_g(0) - \frac{\alpha T^2}{T + \beta},
\end{equation}
where the values for $E_g(0)$ , $\alpha$ and $\beta$ are taken from reference \cite{ALEX}:
\begin{equation}
\begin{gathered}
\label{eq:varshni_params}
E_g(0) = 1.1692 \,\mathrm{eV} \\
\alpha = 4.9    \,\mathrm{eV K^{-1}}									 \\
\beta = 655     \,\mathrm{K}.
\end{gathered}
\end{equation}

```python
temp = np.linspace(1,500,100)
fig, ax = plt.subplots()
ax.plot(temp,E_g_Si(temp)/e)
ax.set_xlabel(r"$T$ (K)")
ax.set_ylabel(r"$E_g$  (eV)")
plt.show()
```

    <IPython.core.display.Javascript object>

### Effectvie masses $m_n$ $m_p$

The following temperature dependencies are used for the effective masses of electrons $m_n$ and for the effective masses of holes $m_h$ \cite{CAIAFA}:
\begin{equation}
\label{eq:me_emp}
m_n = -1.084e-9 T^3 + 7.580e-7 T^2 + 2.862e-4 T + +1.057
\end{equation}
and
\begin{multline}
\label{eq:mh_emp}
m_p = 1.872e-11 T^4 - 1.969e-8 T^3 + 5.857e-6 T^2  + +2.712e-4 T + 0.584.
\end{multline}

### Effectvie density of states $N_c$ $N_v$

The above stated temperature dependence of the effective masses allows the computation of the effective density of states \cite{SZE}:
\begin{equation}
\label{eq:eff_dens_N_c}
N_c = 2 \left(\frac{m_{n} k_b T}{2 \pi \hbar^2}\right)^{3/2},
\end{equation}
and
\begin{equation}
\label{eq:eff_dens_N_v}
N_v = 2 \left(\frac{m_{p} k_b T}{2 \pi \hbar^2}\right)^{3/2}.
\end{equation}

```python
temp = np.linspace(0,300,300)
fig, ax = plt.subplots()
ax.plot(temp,m_n_Si(temp)/m_e,label=r"$m_n$")
ax.plot(temp,m_p_Si(temp)/m_e,label=r"$m_p$")
ax.set_xlabel(r"$T$ (K)")
ax.set_ylabel(r"$m$  ($m_e$)")
ax.legend()
plt.show()

fig, ax = plt.subplots()
ax.semilogy(temp,N_c_Si(temp),label=r"$N_c$")
ax.semilogy(temp,N_v_Si(temp),label=r"$N_v$")
ax.set_xlabel(r"$T$ (K)")
ax.set_ylabel(r"$N$  (m$^{-3}$)")
ax.legend()
plt.show()
```

    <IPython.core.display.Javascript object>

    <IPython.core.display.Javascript object>

## The Bulk model (Boltzmann)
The Bulk model class allows to compute properties of a doped semiconductor reyling on the Boltzmann approximation.

### Parameters
In order to compute charge carrier densities and the Fermi level the following parameters need to be specified:

 parameter	 description	 default value

 temp	 temperature 	 $300  \, \mathrm{K}$

 E_g	 bandgap 	 eq. \ref{eq:varshni}

 N_c	 effective density of states in the conduction band	 eq. \ref{eq:eff_dens_N_c}

 N_v	 effective density of states in the valence band	 eq. \ref{eq:eff_dens_N_v}

 E_v	 valence band	 0.

 E_c	 conduction band	 E_v + E_g

 E_A	 acceptor energy level	 E_v + $0.44 \, \mathrm{eV}$ (Boron as acceptor)

 E_D	 donor energy level	 E_c - $0.45  \, \mathrm{eV}$ (Phosphorous as donor)

 g_A	 acceptor degeneracy factor	 4

 g_D	 donor degeneracy factor	 2

 <th

### Computation of charge carries
If the Fermi level is known, charge carriers can be computed using the Boltzmann approximation \cite{SZE}:
\begin{equation}
\label{n_dop}
n = N_c \exp{\left(\frac{E_c - E_f}{k_b T}\right)} ,
\end{equation}
\begin{equation}
\label{p_dop}
p = N_v \exp{\left(\frac{E_f - E_v}{k_b T}\right)} ,
\end{equation}

```python
#%% calculate charge carrier densities N with Boltzmann approximation
ef = np.linspace(0,1.2,100)*e
bulk = BulkModel(temp = 200.)
fig, ax = plt.subplots()
ax.semilogy(ef/e,bulk.n(ef),label='n')
ax.semilogy(ef/e,bulk.p(ef),label='p')
ax.legend()
ax.set_xlabel(r"$E_f$ (eV)")
ax.set_ylabel(r"$N$ (m$^{-3}$)")
plt.show()
```

    <IPython.core.display.Javascript object>

### Computation of Fermi level

The Fermi level is computed requiring the charge neutrality condition.
\begin{equation}
\label{eq:char_neutr}
n + N_A ^- = p + N_D^+ ,
\end{equation}
where incomplete ionization is considered as follows \cite{SZE}:
\begin{equation}
\begin{aligned}
\label{eq:ionized}
N_A^- = \frac{N_A}{1+ g_A \exp{\left((E_A-E_f)/ (k_b T)\right)}} 		\\
N_D^+ = \frac{N_D}{1+ g_D \exp{\left((E_f-E_D)/ (k_b T)\right)}}.
\end{aligned}
\end{equation}

```python
#%% calculate the Fermi level at 100 K
bulk = BulkModel(temp = 100., N_A = 1e23, N_D =1e18) # units are always SI basic units
print(bulk.E_f/e)
fig, ax = plt.subplots()
ax.axhline(bulk.E_v / e, color='k', label='E_v')
ax.axhline(bulk.E_c / e, color='k', label='E_c')
ax.axhline(bulk.E_f / e, color='r', label='E_f')
ax.legend()
ax.set_ylabel(r"$E$ (eV)")
plt.show()
```

    0.04275767943401833

    <IPython.core.display.Javascript object>

### Calculation of charge carrier in a doped semiconductor

To calculate the charge carriers in a doped semiconductor, first the Fermi energy is computed with eq. \ref{eq:char_neutr}, then carrier densities can be computed using eq. \ref{n_dop} and eq. \ref{p_dop}.

```python
#%% calculate n and p at 200 Kelvin for a certain doping level
bulk = BulkModel(temp = 200., N_A = 1e23, N_D =1e18) # units are always SI basic units
print(bulk.bulk_n , bulk.bulk_p)

#%% plot n,p as a function of N_A
N_A_values = np.logspace(12,22,100)
fig, ax = plt.subplots()
for N_A in N_A_values:
    bulk = BulkModel(temp = 200., N_A = N_A, N_D =0)
    ax.scatter(N_A, bulk.bulk_n,color='b')
    ax.scatter(N_A, bulk.bulk_p,color='r')
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_ylim(1e-2,1e22)
ax.set_xlabel(r"$N_A$ (m$^{-3}$)")
ax.set_ylabel(r"$n / p$ (m$^{-3}$)")
plt.show()
```

    0.02676458460485093 7.073114976468905e+22

    <IPython.core.display.Javascript object>

## The Bulk model (Fermi-Dirac)

The Fermi-Dirac bulk model allows the calculation of charge carriers using Fermi Dirac integrals. The BulkModelFD is a child class that inherits all the paramters and methods from the parent class BulkModel. The calculation of charge carriers is, however, overwritten to be computed with Fermi Dirac integrals.
The electron density is computed via \cite{SZE}:
\begin{equation}
\label{eq:n_fd}
n = \int_{E_c}^{\infty} f(E) D_{c}(E) dE,
\end{equation}
where $f(E)$ is the Fermi function:
\begin{equation}
f(E) = \frac{1}{1 + \exp{\left(\frac{E - E_f}{k_b T} \right)}},
\end{equation}
and $D_c(E)$ the density of states in the conduction band. $D_c(E)$ is linked to the effective density of states in the conduction band $N_c$ via:
\begin{equation}
\label{eq:dc_fd}
D_c = \frac{2}{\sqrt{\pi}} \frac{N_c}{(k_b T )^{3/2}}.
\end{equation}

Analogously the concentration of holes is computed via \cite{SZE}:
\begin{equation}
\label{eq:p_fd}
p = \int_{-\infty}^{E_v} (1-f(E)) D_{v}(E) dE,
\end{equation}
where the density of states in the valence band $D_c(E)$ is linked to the effective density of states in the valence band $N_v$ via:
\begin{equation}
\label{eq:dv_fd}
D_v = \frac{2}{\sqrt{\pi}} \frac{N_v}{(k_b T )^{3/2}}.
\end{equation}

The integrals in eq. \ref{eq:n_fd} and eq. \ref{eq:p_fd} can not be solved analytically.

```python
bulk_dirac = BulkModelFD(temp=100., N_A= 1e20)
bulk       = BulkModel(temp=100., N_A= 1e20)
#%% compare Bolzmann and Fermi Dirac model: charge carriers and Fermi energy
print(bulk_dirac.bulk_p , bulk.bulk_p , bulk_dirac.bulk_p/bulk.bulk_p)
print(bulk_dirac.bulk_n , bulk.bulk_n , bulk_dirac.bulk_n/bulk.bulk_n)
print(bulk_dirac.E_f/e , bulk.E_f/e , bulk_dirac.E_f/bulk.E_f)
```

    9.75299181000535e+19 9.752994999403181e+19 0.9999996729827269
    3.624609531172574e-30 3.624657518472814e-30 0.9999867608732699
    0.08762266890485135 0.08762278299153486 0.9999986979792285

## The QV model (Boltzmann)

This class is a parent class to the BulkModel (Boltzmann) class. It allows the computation of carrier concentrations in a MOS capacitor. The model relies on the charge sheet and fully depletion approximation and the main equations are taken from \cite{BECKERS}.

### Parameters
The following additional parameters need to be defined:

 parameter	 description	 default value

 chi	 electron affinity 	 $4.05  \, \mathrm{eV}$

 phi_m	 metal work function 	 chi

 cox	 oxide capacitance 	 $10  \, \mathrm{mFm^{-2}}$

 <th

### Metal-semiconductor work function $\phi_{ms}$
$\phi_{ms}$ is computed via:
\begin{equation}
\phi_{ms} = \phi_m - \phi_s
\end{equation} ,
where
\begin{equation}
\phi_{s} = E_c - E_f + \chi
\end{equation}.

### The surface, acceptor and bulk potential
The surface potential $\psi_s$ is a central quantity for the calculation of charge carriers.
It should be noted, that all of the energies and potentials in the python model are linked to the Fermi energy $E_f$. In this convention the surface potential $\psi_s$ is defined as:
\begin{equation}
\label{eq:psisdef2}
\psi_{s} = \frac{E_{f} - E_{i,s}}{e},
\end{equation}
wherer $E_{i,s}$ describes the intrinsic energy at the semiconductor oxide interface. Once the surface potential as a function of the gate voltage is computed, charge carrier density (sheet densities) in the channel can be calculated which allows in the following the calculation of a drain to source current.
Analougsly the acceptor $\psi_A$ and bulk potential $\psi_b$ can be defined as follows:
\begin{equation}
\label{eq:psia}
\psi_{A} = \frac{E_{A} - E_{i}}{e},
\end{equation}
\begin{equation}
\label{eq:psib}
\psi_{b} = \frac{E_{f} - E_{i,b}}{e}.
\end{equation}

It should be noted that $\psi_A$ and $\psi_b$ do not depend on any external bias and can thus be calculated right away.

```python
qv = BeckersQVpy(temp = 100. , N_A = 1e22)
print(qv.psi_a, qv.psi_b)
```

    -0.5340199039387991 -0.5235526838675402

### The electric field at the surface $E_{f,s}$

With the above defined potentials the electric field at the semiconductor oxide interface of the transistor can be calculated as follows \cite{BECKERS}:

\begin{multline}
\label{eq:es2}
\mathcal{E}_{s}^2 = \frac{2 e n_i \phi_t}{\epsilon_{si}} \left(\exp{(\frac{\psi_{s} - V_{ch}}{\phi_t})} - \exp{(\frac{\psi_{b} - V_{ch}}{\phi_t})} + \exp{(\frac{-\psi_{s}}{\phi_t})} - \exp{(\frac{-\psi_{b}}{\phi_t})}\right) \\
+ \frac{2 e N_A}{\epsilon_{si}} \left(\psi_s - \psi_b - \phi_t \log{\left(\frac{1 + g_A \exp{\frac{\psi_A - \psi_b}{\phi_t}}}{1 + g_A \exp{\frac{\psi_A - (\psi_s - \psi_{b})}{\phi_t}}}\right)}\right),
\end{multline}
whrere $\phi_t = \frac{k_b T}{e}$ is the thermal voltage, and $V_{ch}$ the channel voltage (see section?)

### The potential loop
The potential loop equation links the surface potential $\psi_s$ to the applied gate voltage $V_{GB}$ \cite{BECKERS}:
\begin{equation}
\label{eq:pot_loop2}
V_{GB} = (\psi_s - \psi_b )  + \phi_{ms} + \epsilon_{si} \mathcal{E}_{s}(\psi_{s}) / C_{ox} + Q_0 / C_{ox}.
\end{equation}

This equation is solved implicitly for every gate voltage.

```python
vg = np.linspace(0,1.5,100)
qv = BeckersQVpy(temp = 100. , N_A = 1e22)
fig, ax = plt.subplots()
ax.plot(vg,qv.psi_s(0,vg), label= r"$V_{ch}$ = 0 V")
ax.plot(vg,qv.psi_s(0.5,vg), label= r"$V_{ch}$ = 0.5 V")
ax.plot(vg,qv.psi_s(1,vg), label= r"$V_{ch}$ = 1 V")
ax.legend()
ax.set_xlabel(r"$V_{GB}$ (V)")
ax.set_ylabel(r"$\psi_{s}$ (V)")
plt.show()
```

    <IPython.core.display.Javascript object>

### Computing the mobile channel charge $Q_m$
Once $\psi_s$ is determined as a function of the gate voltage, the mobile channel charge can be computed \cite{BECKERS}.

\begin{equation}
Q_m = \frac{\epsilon_{si} \mathcal{E}_{s}}{C_{ox}} +  \epsilon_{si} \sqrt{\frac{2 e N_A (\psi_{s} - \psi_{b})}{\epsilon_{si}} - \frac{2 e N_A \phi_t}{\epsilon_{si}} \phi_t \log{\left(\frac{1 + g_A \exp{\frac{\psi_A - \psi_b}{\phi_t}}}{1 + g_A \exp{\frac{\psi_A - (\psi_s - \psi_{b})}{\phi_t}}}\right)}}
\end{equation}

```python
vg = np.linspace(0,0.8,200)
qv = BeckersQVpy(temp = 100. , N_A = 1e22)
fig, ax = plt.subplots()
ax.plot(vg,qv.Q_m(qv.psi_s(0, vg),0), label= r"$V_{ch}$ = 0 V")
ax.set_xlabel(r"$V_{GB}$ (V)")
ax.set_ylabel(r"$Q_m$ (Cm$^{-2}$)")
plt.show()
```

    <IPython.core.display.Javascript object>

## The QV model (Fermi Dirac)

The DiracQVpy class is a child class to the BeckersQVpy class and BulkModelFD class and inherits all the methods and paramters. This means that charge carriers are computed with the Fermi Dirac integrals.
The calculation of $E_s$ is overwritten in this class to be:
\begin{multline}
\label{eq:FD_psis}
\mathcal{E}_{s}^{2} = \frac{2 e}{\epsilon_{si}} \int_{\psi_b}^{\psi_s} \left(-\int_{E_c}^{\infty} \frac{1}{1+ \exp{\left(\frac{E-e \psi - E_i}{k_b T}\right)}} D_{c}(E) dE + \right. \\
 \left. \int_{-\infty}^{E_v} \left(1- \frac{1}{1+ \exp{\left(\frac{E-e \psi - E_i}{k_b T}\right)}}\right) D_{v}(E) dE - N_{A}^{-}  \right)  d\psi
\end{multline}

The rest of the computations stays the same.

```python
# attention this takes some time to run...
vg = np.linspace(0,0.8,200)
qv = BeckersQVpy(temp = 100. , N_A = 1e22)
qv_dirac = DiracQVpy(temp = 100. , N_A = 1e22)

fig, ax = plt.subplots()
ax.plot(vg,qv.Q_m(qv.psi_s(0, vg),0), label= r"Boltzmann")
ax.plot(vg,qv_dirac.Q_m(qv_dirac.psi_s(0, vg),0), label= r"Dirac",linestyle='--')
ax.set_xlabel(r"$V_{GB}$ (V)")
ax.set_ylabel(r"$Q_m$ (Cm$^{-2}$)")
plt.show()

fig, ax = plt.subplots()
ax.plot(vg,qv.psi_s(0,vg), label= r"$V_{ch}$ = 0 V")
ax.plot(vg,qv_dirac.psi_s(0,vg), label= r"$V_{ch}$ = 0 V",linestyle='--')
ax.plot(vg,qv.psi_s(0.5,vg), label= r"$V_{ch}$ = 0.5 V")
ax.plot(vg,qv_dirac.psi_s(0.5,vg), label= r"$V_{ch}$ = 0.5 V",linestyle='--')
ax.legend()
ax.set_xlabel(r"$V_{GB}$ (V)")
ax.set_ylabel(r"$\psi_{s}$ (V)")
plt.show()
```

    <IPython.core.display.Javascript object>

    <IPython.core.display.Javascript object>

## The IV model (Boltzmann)

The previously introduced QV classes allow for the computation of the mobile channel charge $Q_m$. In the IV model also drain to source currents can be calculated. The class is a child class to the QV class.

### Parameters
The following additional parameters need to be defined:

 parameter	 description	 default value

 u0	 zero field mobility 	 $0.045 \, \mathrm{cm^{2}V^{-1}s^{-1}}$

 theta	 mobility reduction paramter 	 0

 mobmod	 mobility model selection parameter 	 0

 w	 transistor width	 None

 l	 transistor length	 None

 <th

### The channel voltage $V_{ch}$

When a drain to source voltage $V_{DS}$ is applied and an inversion layer is present, electrons can be transported along the channel. This creates a non-equilibrium situation for the electrons, that can be incorporated in our model via the introduction of the channel voltage $V_{ch}$ \cite{WU}. The channel voltage has a value of $V_{DS}$ right at the drain side of the transistor and drops to $V_S$ at the source side of the transistor and is thus a function of the horizontal position $x$.
$V_{ch}$  shifts the potential $\psi_s$ in the calculation of the electron concentration  $n$
in eq. \ref{eq:es2}.
The computation for $\psi_s$ and $E_{s}$ now also depends on the horizontal position $x$. To calculate a current flowing from source to drain an integral along this axis ($x$) will be performed as will be shown in the next section.

### Effective mobility $\mu_{eff}$ as a function of the gate voltage
Per default a very simple mobility model (mobmod==0) is used:
\begin{equation}
\label{eq:ids_mudef}
\mu_{eff}(V_{GB}) = \frac{\mu_{0}}{1 + \theta V_{GB}}.
\end{equation},
however, several more sophisticated mobility models are implemented as well (modmod== 1,2), for details see the definition of $\mu_{eff}$ in IV.py .

### The drain to source current
An expression for $I_{DS}$ starting from drift and diffusion transport equation can be written in an integral form in terms of the channel voltage $V_{ch}$ \cite{ENZ}:

\begin{equation}
I_{DS} = - \mu_{eff}(V_{GB})  \int_{V_{SB}}^{V_{DB}} Q_I(V_{ch}) dV_{ch}.
\end{equation}

```python
iv = BeckersIVpy(temp=50.,N_A=1e22,cox = 20e-3, w=10e-6,l=10e-6)
vg = np.linspace(0,1.0,100)
vds = 0.050
fig, ax = plt.subplots()
ax2 = ax.twinx()
ax.plot(vg,iv.i_ds(vds,vg))
ax2.semilogy(vg,iv.i_ds(vds,vg))
ax.set_xlabel(r"$V_{GB}$ (V)")
ax.set_ylabel(r"$I_{DS}$ (A)")
ax2.set_ylabel(r"$I_{DS}$ (A)")
plt.show()
```

    <IPython.core.display.Javascript object>

```python
#%% plot currents for different temperatures
from matplotlib import cm
from matplotlib.colors import Normalize
norm = Normalize(vmin=0, vmax=305)
my_cmap = cm.plasma

fig, ax = plt.subplots()
vg = np.linspace(0,0.4,1000)
vds = 0.050
temp_lis = [20,50,100,150,200,300]
for temp in temp_lis:
    iv = BeckersIVpy(temp=temp,N_A=1e22,cox = 20e-3, w=10e-6,l=10e-6)
    ax.semilogy(vg,iv.i_ds(vds,vg),c = my_cmap(norm(float(temp))))

ax.set_xlabel(r"$V_{GB}$ (V)")
ax.set_ylabel(r"$I_{DS}$ (A)")
plt.show()
```

    <IPython.core.display.Javascript object>

# References

[TSIVIDIS] Yannis Tsividis and Colin McAndrew, ``_Operation and modeling of the MOS transistor_'',  2011.

[BREWS] Brews J. R., ``_A charge-sheet model of the MOSFET_'', Solid-State Electronics, vol. 21, number 2, pp. 345--355,  1978.

[VARSHNI] Varshni Y. P., ``_Temperature dependence of the energy gap in semiconductors_'', Physica, vol. 34, number 1, pp. 149--154,  .

[ALEX] Alex V., Finkbeiner S. and Weber J., ``_Temperature dependence of the indirect energy gap in crystalline silicon_'', Journal of Applied Physics, vol. 79, number 9, pp. 6943--6946,  1996.

[CAIAFA] Caiafa A., Wang X., Hudgins J. L. et al., ``_Cryogenic study and modeling of IGBTs_'', IEEE 34th Annual Conference on Power Electronics Specialist, vol. , number , pp. 1897--1903,  2003.

[SZE] S. M. Sze and Kwok Kwok Ng, ``_Physics of semiconductor devices_'',  2007.

[BECKERS] Beckers Arnout, Jazaeri Farzan and Enz Christian, ``_Cryogenic MOS Transistor Model_'', IEEE Transactions on Electron Devices, vol. 65, number 9, pp. 3617--3625,  2018.  [online](http://arxiv.org/pdf/1806.02142v1)

[WU] Wu W., Chen T.-L., Gildenblat G. et al., ``_Physics-Based Mathematical Conditioning of the MOSFET Surface Potential Equation_'', IEEE Transactions on Electron Devices, vol. 51, number 7, pp. 1196--1200,  2004.

[ENZ] Christian Enz and Eric A. Vittoz, ``_Charge-based MOS transistor modeling: The EKV model for low-power and RF IC design_'',  2006.


# Übungsblatt 3: sWeights

 * [Aufgabe 1](#Aufgabe-1)
 * [Aufgabe 2](#Aufgabe-2)
---

```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
from scipy.stats import norm

plt.style.use('ggplot')
```

---
Eine experimentelle Verteilung in den Variablen $(x, m)$ habe eine Signalkomponente $s(x, m)$ = $s(x)s(m)$ und eine Untergrundkomponente $b(x,m)$ = $b(x)b(m)$. Der erlaubte Bereich ist $0 < x < 1$ und $0 < m < 1$. Es sei $s(m)$ eine Gaussverteilung mit Mittelwert $\mu = 0.5$ und Standardabweichung $\sigma = 0.05$. Die Verteilungen der anderen Komponenten werden aus gleichverteilten Zufallzahlen $z$ gewonnen. Für $s(x)$ verwende man $x = −0.2\ln{z}$, für $b(m)$ verwende man $m = \sqrt{z}$ und für $b(x)$ die Transformation $x = 1 − \sqrt{z}$.

Erzeugen Sie für zwei angenommene Effizienzfunktionen

 * $\varepsilon(x, m) = 1$
 * $\varepsilon(x, m) = (x + m) / 2$

Datensätze von Paaren $(x, m)$ die 20000 akzeptierte Signalereignisse und 100000 akzeptierte Untergrundereignisse umfassen.
Betrachten Sie nun die gemeinsame $m$-Verteilung und parametrisieren Sie diese durch

\begin{equation}
    f(m) = s(m) + b(m)
\end{equation}

mit

\begin{equation}
    s(m) = p_0 \exp\left(-\frac{(m - p_1)^2}{2p_2^2}\right)
\end{equation}

und

\begin{equation}
    b(m) = p_3 + p_4m + p_5m^2 + p_6\sqrt{m} \,.
\end{equation}

 Für den Fall $\varepsilon(x, m) = (x + m)/2$ benutzen Sie die obige Parametrisierung auch zur Beschreibung der $m_c$ und $m_{cc}$-Verteilungen, für die jeder $m$-Wert mit $1/\varepsilon(x, m)$, bzw. $1/\varepsilon^2(x, m)$ gewichtet wird, und die für die korrekte Behandlung von nicht-konstanten Effizienzen benötigt werden.

---

```python
def generate_sx(size):
    xs = -0.2 * np.log(np.random.uniform(size=2 * size))
    xs = xs[xs < 1]
    return xs[:size]

def generate_sm(size):
    return np.random.normal(0.5, 0.05, size=size)

def generate_s(size):
    return np.array([generate_sx(size), generate_sm(size)])

def generate_bx(size):
    return 1 - np.sqrt(np.random.uniform(size=size))

def generate_bm(size):
    return np.sqrt(np.random.uniform(size=size))

def generate_b(size):
    return np.array([generate_bx(size), generate_bm(size)])

def generate_sample(sig_size=20000, bkg_size=100000):
    return np.append(generate_s(sig_size), generate_b(bkg_size), axis=1)

def efficiency(x, m):
    return (x + m) / 2

def generate_with_efficiency(generator, efficiency, size):
    def reset():
        xs, ms = generator(size)
        effs = efficiency(xs, ms)
        accept = np.random.uniform(size=size) > effs
        return np.array([xs[accept], ms[accept]])

    sample = reset()
    while sample.shape[1] < size:
        sample = np.append(sample, reset(), axis=1)
    return sample[:size]

def generate_sample_with_efficiency(efficiency, sig_size=20000, bkg_size=100000):
    return np.append(generate_with_efficiency(generate_s, efficiency, sig_size),
                     generate_with_efficiency(generate_b, efficiency, bkg_size),
                     axis=1)

n = 20000
xs, ms = generate_sample()
xs_s, xs_b = xs[:n], xs[n:]
ms_s, ms_b = ms[:n], ms[n:]

plt.hist([xs_s, xs_b], bins=40, histtype='barstacked', label=['Signal', 'Background'])
plt.xlabel(r'$x$')
plt.legend()
plt.show()

plt.hist([ms_s, ms_b], bins=40, histtype='barstacked', label=['Signal', 'Background'])
plt.xlabel(r'$m$')
plt.legend()
plt.show()

effs = efficiency(xs, ms)
effs_s, effs_b = effs[:n], effs[n:]
plt.hist([effs_s, effs_b], bins=40, histtype='barstacked', label=['Signal', 'Background'])
plt.xlabel(r'$\varepsilon$')
plt.legend()
plt.show()

exs, ems = generate_sample_with_efficiency(efficiency)
exs_s, exs_b = exs[:n], exs[n:]
ems_s, ems_b = ems[:n], ems[n:]

plt.hist([exs_s, exs_b], bins=40, histtype='barstacked', label=['Signal', 'Background'])
plt.xlabel(r'$x$')
plt.legend()
plt.show()

plt.hist([ems_s, ems_b], bins=40, histtype='barstacked', label=['Signal', 'Background'])
plt.xlabel(r'$m$')
plt.legend()
plt.show()
```

---
## Aufgabe 1
Bestimmen Sie für beide Effizienzfunktion die sWeights $w(m)$ aus den beobachteten $m$-Verteilungen, und verwenden Sie $w(m)/\varepsilon(x, m)$ um die Verteilung $N_{s}s(x)$ aus den Daten heraus zu projizieren. Vergleichen Sie für beide Effizienzfunktionen das Resultat mit der Erwartung.

---

Zunächst fitten wir die kombinierte Massenverteilung von Signal und Untergrund an unsere beiden Datensätze. Dabei müssen wir daran denken, die Anzahl Signal- und Untergrundereignissen als Fitparamter zu behandeln. Betrachten wir zuerst den Fall $\varepsilon = 1$.

```python
hist, medges, xedges = np.histogram2d(ms, xs, bins=(100, 140), range=((0, 1), (0, 1)))
mwidth = medges[1] - medges[0]
mcentres = medges[:-1] + mwidth / 2
xwidth = xedges[1] - xedges[0]
xcentres = xedges[:-1] + xwidth / 2

mhist = np.sum(hist, axis=1)
xhist = np.sum(hist, axis=0)

plt.plot(mcentres, mhist, '.', label='$m$')
plt.plot(xcentres, xhist, '.', label='$x$')
plt.ylabel('Absolute Häufigkeit')
plt.legend()
plt.show()
```

```python
def pdf_ms(m, p0, p1, p2):
    return p0 * np.exp(-(m - p1) ** 2 / 2 / p2 ** 2)

def pdf_mb(m, p3, p4, p5, p6):
    return p3 + p4 * m + p5 * m ** 2 + p6 * np.sqrt(m)

def pdf_m(m, p0, p1, p2, p3, p4, p5, p6):
    return pdf_ms(m, p0, p1, p2) + pdf_mb(m, p3, p4, p5, p6)

def fit_mass(centres, ns, pars=None):
    if pars is None:
        pars = [20000, 0.5, 0.5, 100000, 0.1, 0, 1]
    return curve_fit(pdf_m, centres, ns, p0=pars)

popt, _ = fit_mass(mcentres, mhist)

plt.plot(mcentres, mhist, '.')
plt.plot(mcentres, pdf_m(mcentres, *popt))
plt.plot(mcentres, pdf_ms(mcentres, *popt[:3]), '--')
plt.plot(mcentres, pdf_mb(mcentres, *popt[3:]), '--')
plt.xlabel('$m$')
plt.show()
```

Als nächstes können wir mit Hilfe der bestimmten Parameter die sWeights bestimmen.

```python
def sweights(centres, mhist, popt):
    s = pdf_ms(centres, *popt[:3])
    b = pdf_mb(centres, *popt[3:])
    n = mhist

    # Normierung der PDFs
    s = s / np.sum(s)
    b = b / np.sum(b)

    Wss = np.sum((s * s) / n)
    Wsb = np.sum((s * b) / n)
    Wbb = np.sum((b * b) / n)
    alpha = Wbb / (Wss * Wbb - Wsb ** 2)
    beta = -Wsb / (Wss * Wbb - Wsb ** 2)
    weights = (alpha * s + beta * b) / n
    return weights

sw = sweights(mcentres, mhist, popt)

plt.plot(mcentres, sw, '.')
plt.xlabel('$m$')
plt.ylabel('sWeight')
plt.show()
```

Diese können wir nun verwenden, um die Signalkomponente $s(x)$ herauszuprojizieren.

```python
def apply_sweights(sweights, hist):
    return np.array([w * row for w, row in zip(sweights, hist)]).sum(axis=0)

xweighted = apply_sweights(sw, hist)
plt.plot(xcentres, xweighted, '.', label='sWeighted')
plt.plot(xcentres, xhist, '.', label='s+b')
plt.xlabel('$x$')
plt.ylabel('Häufigkeit')
plt.yscale('log')
plt.legend(loc='lower left')
plt.show()
```

Für $\varepsilon = (x + m) / 2$ verwenden wir an dieser Stelle fälschlicherweise genau das gleiche Vorgehen.

```python
ehist, emedges, exedges = np.histogram2d(ems, exs, bins=(100, 140), range=((0, 1), (0, 1)))
exwidth = exedges[1] - exedges[0]
emwidth = emedges[1] - emedges[0]
excentres = exedges[:-1] + exwidth / 2
emcentres = emedges[:-1] + emwidth / 2

emhist = np.sum(ehist, axis=1)
exhist = np.sum(ehist, axis=0)
plt.plot(emcentres, emhist, '.', label='$m$')
plt.plot(excentres, exhist, '.', label='$x$')
plt.ylabel('Häufigkeit')
plt.legend()
plt.show()

epopt, _ = fit_mass(emcentres, emhist, pars=[20000, 0.5, 0.5, 1, 1, -0.1, 100])

plt.plot(emcentres, emhist, '.')
plt.plot(emcentres, pdf_m(emcentres, *epopt))
plt.plot(emcentres, pdf_ms(emcentres, *epopt[:3]), '--')
plt.plot(emcentres, pdf_mb(emcentres, *epopt[3:]), '--')
plt.xlabel('$m$')
plt.show()

esw = sweights(emcentres, emhist, epopt)
plt.plot(emcentres, esw, '.')
plt.xlabel('$m$')
plt.ylabel('sWeight')
plt.show()

exweighted = apply_sweights(esw, ehist)
plt.plot(excentres, exweighted, '.', label='sWeighted')
plt.plot(excentres, exhist, '.', label='s+b')
plt.plot(xcentres, xweighted, '.', label='sWeighted correct')
plt.xlabel('$x$')
plt.ylabel('Häufigkeit')
plt.yscale('log')
plt.legend(loc='lower left')
plt.show()
```

---
## Aufgabe 2
Bestimmen Sie für $\varepsilon(x, m) = (x + m)/2$ unter Berücksichtigung der Funktion $\varepsilon(x, m)$ in der Bestimmung von $w(m)$ die korrekten sWeights aus den mit $1/\varepsilon(x, m)$ gewichteten Daten. Verwenden Sie die korrekten sWeights um mit $w(m)/\varepsilon(x, m)$ um die Verteilung $N_{s}s(x)$ zu extrahieren.

```python
eeffs = efficiency(exs, ems)
ehist, emedges, exedges = np.histogram2d(
    ems,
    exs,
    bins=(100, 140),
    range=((0, 1), (0, 1)),
    weights=1 / eeffs
)
emwidth = emedges[1] - emedges[0]
emcentres = emedges[:-1] + emwidth / 2
exwidth = exedges[1] - exedges[0]
excentres = exedges[:-1] + exwidth / 2

emhist = np.sum(ehist, axis=1)
exhist = np.sum(ehist, axis=0)

plt.plot(emcentres, emhist, 'o', label='$m$')
plt.plot(excentres, exhist, 's', label='$x$')
plt.ylabel('Gewichtete Häufigkeit')
plt.legend()
plt.show()

epopt, _ = fit_mass(emcentres, emhist,
                    pars=[2000, 0.5, 0.5, 1, 1, -0.1, 10])

plt.plot(emcentres, emhist, '.')
plt.plot(emcentres, pdf_m(emcentres, *epopt))
plt.plot(emcentres, pdf_ms(emcentres, *epopt[:3]), '--')
plt.plot(emcentres, pdf_mb(emcentres, *epopt[3:]), '--')
plt.xlabel('$m$')
plt.show()

eeffhist = efficiency(*np.meshgrid(excentres, emcentres))

def sweights_q(centres, qs, popt):
    s = pdf_ms(centres, *popt[:3])
    s = s / np.sum(s)
    b = pdf_mb(centres, *popt[3:])
    b = b / np.sum(b)

    Wss = np.sum((s * s) / qs)
    Wsb = np.sum((s * b) /  qs)
    Wbb = np.sum((b * b) /  qs)
    alpha = Wbb / (Wss * Wbb - Wsb ** 2)
    beta = -Wsb / (Wss * Wbb - Wsb ** 2)
    weights = (alpha * s + beta * b) / qs
    return weights

qs = np.sum(ehist / eeffhist, axis=1)
esw = sweights_q(emcentres, qs, epopt)

plt.plot(emcentres, esw, '.')
plt.xlabel('$m$')
plt.ylabel('sWeight')
plt.show()

exweighted = np.array([s * h for s, h in zip(sw, ehist)]).sum(axis=0)

plt.plot(excentres, exweighted, '.', label='sWeighted')
plt.plot(excentres, exhist, '.', label='s+b')
plt.plot(xcentres, xweighted, '.', label='sWeighted correct')
plt.xlabel('$x$')
plt.ylabel('Häufigkeit')
plt.yscale('log')
plt.legend(loc='lower left')
plt.show()
```


## First Assignment

#### 1) Apply the appropriate string methods to the **x** variable (as '.upper') to change it exactly to: "$Dichlorodiphenyltrichloroethane$".

```python
x = "DiClOrod  IFeNi  lTRicLOr oETaNo  DiChlorod iPHeny lTrichL oroEThaNe"
```

```python
# first try. there might be a way where I don't need to create now variables for each step?
xnew = x.replace(" ", "")
xnew_stripped = xnew[27:]
xnew_stripped_lower = xnew_stripped.lower()
xfinal = xnew_stripped.capitalize()
print(xfinal)
```

    Dichlorodiphenyltrichloroethane

```python
#other solution presented in class(not my own work):
y = "DiClOrod  IFeNi  lTRicLOr oETaNo  DiChlorod iPHeny lTrichL oroEThaNe"
y.replace(" ", "")[27: ].title()

```

    'Dichlorodiphenyltrichloroethane'

#### 2) Assign respectively the values: 'word', 15, 3.14 and 'list' to variables A, B, C and D in a single line of code. Then, print them in that same order on a single line separated by a space, using only one print statement.

```python
a_dict={"A":"word","B":15,"C":3.14,"D":"list"}
print(a_dict.values())

#from office hours: we should do this with tuples. but it works like this as well?
```

    dict_values(['word', 15, 3.14, 'list'])

```python
#so second try with tuple:
A, B, C, D = "word", 15, 3.14, "list"
print(A, B, C, D)
```

#### 3) Use the **input()** function to receive an input in the form **'68.4 1.71'**, that is, two floating point numbers in a line separated by space. Then, assign these numbers to the variables **w** and **h** respectively, which represent an individual's weight and height (hint: take a look at the '.split()' method). With this data, calculate the individual's Body Mass Index (BMI) from the following relationship:

\begin{equation}
BMI = \dfrac{weight}{height^2}
\end{equation}

```python
input_text = input(float, )

split_text = input_text.split(" ")
print(split_text)
input_text.append("w", "h")

#assign Weight to first value und height to second. HOW? with dictionary? see below.
float(split_text)

bmi = w/h**2

#second try (without input() function and without beiing able to convert strings to floats)
#does not work
my_string = "55,0 167,7"
float(my_string)

my_string = "55,0 167,7"
float(my_string)
my_split_list = my_string.split(" ")
print(my_split_list)
int(my_split_list)
dimension_dic = {"w": my_split_list [0], "h": my_split_list [1]}
['55,0', '167,7']

bmi = dimension_dic["w"]/dimension_dic["h"]**2
['55,0', '167,7']

```

```python
#solution:
input_w = input("enter your weight in kg:" )
input_h = input("enter your height in m:" )

float_w = float(input_w)
float_h = float(input_h)

bmi = float_w/(float_h**2)
print("This is your BMI:", bmi)
```

    enter your weight in kg: 55
    enter your height in m: 170

    This is your BMI: 0.0019031141868512112

#### This value can also be classified according to ranges of values, following to the table below. Use conditional structures to classify and print the classification assigned to the individual.

<\center>

(source: https://healthtravelguide.com/bmi-calculator/)

```python
input_w = input("enter your weight in kg:" )
input_h = input("enter your height in m:" )

float_w = float(input_w)
float_h = float(input_h)

bmi = float_w/(float_h**2)

print("This is your bmi:", bmi)

if (bmi < 18.5):
        print ("Your nutritional status is 'Underweight'")
elif (bmi >= 18.5) and (bmi <= 24.9):
         print ("Your nutritional status is 'Normal weight'")
elif (bmi >= 25.0) and (bmi <= 29.9):
         print("Your nutritional status is 'Pre-obesity'")
elif (bmi >= 30.0) and (bmi <= 34,9):
        print("Your nutritional status is 'Obersity class I'")
elif (bmi >= 35.0) and (bmi <= 39,9):
        print("Your nutritional status is 'Obersity class II'")
elif (bmi >= 40):
        print("Your nutritional status is 'Obersity class III'")
else:
        print("your BMI is to low to calculate")
```

    enter your weight in kg: 50
    enter your height in m: 140

    This is your bmi: 0.002551020408163265
    Your nutritional status is 'Underweight'

#### 4) Receive an integer as an input and, using a loop, calculate the factorial of this number, that is, the product of all the integers from one to the number provided.

```python

```

#### 5) Using a while loop and the input function, read an indefinite number of integers until the number read is -1. Present the sum of all these numbers in the form of a print, excluding the -1 read at the end.

```python
#use break statement at the end? or continue
```

#### 6) Read the **first name** of an employee, his **amount of hours worked** and the **salary per hour** in a single line separated by commas. Next, calculate the **total salary** for this employee and show it to two decimal places.

```python
#first try. only one decimal place
x = ("Nora", 40, 10)
(name, hours, salery) = x
name = x[0]
hours = x[1]
salery= x[2]

salerytotal = float(hours*salery)
print(salerytotal)
```

    400.0

```python
#google to show 2 decimals.
#works
x = ("Nora", 40, 10)
(name, hours, salery) = x
hours = x[1]
salery = x[2]

salerytotal = (hours*salery)
print(format(salerytotal, ".2f"))

```

    400.00

#### 7) Read three floating point values **A**, **B** and **C** respectively. Then calculate itens a, b, c, d and e:

```python

```

    a) the area of the triangle rectangle with A as the base and C as the height.

```python

```

    b) the area of the circle of radius C. (pi = 3.14159)

```python

```

    c) the area of the trapezoid that has A and B for bases and C for height.

```python

```

    d) the area of the square that has side B.

```python

```

    e) the area of the rectangle that has sides A and B.

```python

```

#### 8) Read **the values a, b and c** and calculate the **roots of the second degree equation** $ax^{2}+bx+c=0$ using [this formula](https://en.wikipedia.org/wiki/Quadratic_equation). If it is not possible to calculate the roots, display the message **“There are no real roots”**.

```python

```

#### 9) Read four floating point numerical values corresponding to the coordinates of two geographical coordinates in the cartesian plane. Each point will come in a line with its coordinates separated by space. Then calculate and show the distance between these two points.

(obs: $d=\sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}$)

```python

```

    'you can type anything'

#### 10) Read **two floating point numbers** on a line that represent **coordinates of a cartesian point**. With this, use **conditional structures** to determine if you are at the origin, printing the message **'origin'**; in one of the axes, printing **'x axis'** or **'y axis'**; or in one of the four quadrants, printing **'q1'**, **'q2**', **'q3'** or **'q4'**.

```python

```

#### 11) Read an integer that represents a phone code for international dialing.
#### Then, inform to which country the code belongs to, considering the generated table below:
(You just need to consider the first 10 entries)

```python
import pandas as pd
df = pd.read_html('https://en.wikipedia.org/wiki/Telephone_numbers_in_Europe')[1]
df = df.iloc[:,:2]
df.head(20)
```

      Country
      Country calling code

      0
      Austria
      43

      1
      Belgium
      32

      2
      Bulgaria
      359

      3
      Croatia
      385

      4
      Cyprus
      357

      5
      Czech Republic
      420

      6
      Denmark
      45

      7
      Estonia
      372

      8
      Finland
      358

      9
      France
      33

      10
      Germany
      49

      11
      Greece
      30

      12
      Hungary
      36

      13
      Iceland
      354

      14
      Ireland
      353

      15
      Italy
      39

      16
      Latvia
      371

      17
      Liechtenstein
      423

      18
      Lithuania
      370

      19
      Luxembourg
      352

```python

```

#### 12) Write a piece of code that reads 6 numbers in a row. Next, show the number of positive values entered. On the next line, print the average of the values to one decimal place.

```python

```

#### 13) Read an integer **N**. Then print the **square of each of the even values**, from 1 to N, including N, if applicable, arranged one per line.

```python

```

#### 14) Using **input()**, read an integer and print its classification as **'even / odd'** and **'positive / negative'** . The two classes for the number must be printed on the same line separated by a space. In the case of zero, print only **'null'**.

```python
#first idea
if x / 2 == 0
    print("even")
if not
    print("odd")
if x > 0
    print("positive")
if not
    print("negative")
if  x == 0
    print("null")
pass

#how do I get the results in one line? is there like cache storage for the duration of the call?
```

```python
#second try with function works but missing the input part.
def function(x):
    if x%2 == 0 and x > 0:
        return "even positive"
    if x%2 != 0 and x > 0:
        return "odd positive"
    if x%2 == 0 and x < 0:
        return "even negative"
    if x%2 != 0 and x < 0:
        return "odd negative"
    if x == 0:
        return "null"

function(0)

```

    'null'

```python
#thrid try with input:
def function_evenodd(*args):
    if args == 0:
        args = input("Input your number here:" )
    if args%2 == 0 and args > 0:
        return "even positive"
    if args%2 != 0 and args > 0:
        return "odd positive"
    if args%2 == 0 and args < 0:
        return "even negative"
    if args%2 != 0 and args < 0:
        return "odd negative"
    if args == 0:
        return "null"

```

```python
function_evenodd()
```

```python
#use a generator function?
```

## Challenge
#### 15) Ordering problems are recurrent in the history of programming. Over time, several algorithms have been developed to fulfill this function. The simplest of these algorithms is the [**Bubble Sort**](https://en.wikipedia.org/wiki/Bubble_sort), which is based on comparisons of elements two by two in a loop of passes through the elements. Your mission, if you decide to accept it, will be to input six whole numbers ramdonly ordered. Then implement the **Bubble Sort** principle to order these six numbers **using only loops and conditionals**.
#### At the end, print the six numbers in ascending order on a single line separated by spaces.

```python

```