<jupyter_start><jupyter_text>Diodes===The incident flux and the current that is generated by a photodiode subjected to it are related by$$\begin{equation}\begin{split}I(A)=&\sum_{i,j}P_{i,j}(W)R_{j}(A/W)+D(A)\\P_{i,j}(W)=&I_{i,j}(Hz)E_{j}(\text{keV})\\R_{j}(A/W)=&\frac{e(C)}{E_{h}(\text{keV})}[1-e^{-\mu(E_{j})\rho d}]\end{split}\end{equation}$$where P the incident power, R the spectral responsivity, D the dark current, $E_i$ the energy of the incident photon, $E_j$ the energy of the detected photon, $E_{h}$ the energy to create an electron-hole pair, $I_{i,j}$ the detected flux of line $j$ due to line $i$ and diode density $\rho$, mass attenuation coefficient $\mu$ and thickness $d$.The relationship between the detected flux and the flux at the sample position is given by$$\begin{equation}I_{i,j}(Hz)=I_{0}(Hz) w_i Y_{i,j} = I_{s}(Hz)\frac{w_i Y_{i,j}}{\sum_k w_k T_{s}(E_{k})}\end{equation}$$with the following factors* $I_0$: total flux before detection* $I_s$: the total flux seen by the sample* $T_s$: the "transmission" between source and sample (product of several transmission factors and optics efficiency)* $w_k$: the fraction of primary photons with energy $E_{k}$* $Y_{i,j}$: the "rate" of detected line $j$ due to source line $i$ (not including detector attenuation)The line fractions at the sample position are$$\begin{equation}\begin{split}I_{i,s}=& I_0 w_i T_{s}(E_{i})\\w_{i,s} =& \frac{I_{i,s}}{\sum_k I_{k,s}} = \frac{w_i T_{s}(E_{i})}{\sum_k w_k T_{s}(E_{k})}\end{split}\end{equation}$$The relationship between the flux reaching the sample and the current measured by a pn-diode can be summarized as$$\begin{equation}\begin{split}I(A)=&I_{s}(Hz)C_s(C)+D(A)\\C_s(C) =& \frac{\sum_{i,j} w_i Y_{i,j}C_j}{\sum_k w_k T_{s}(E_{k})}\\C_j(C) =& E_{j}(\text{keV})\frac{e(C)}{E_{h}(\text{keV})}[1-e^{-\mu(E_{j})\rho d}]\\\end{split}\end{equation}$$where $C_s$ the charge generated per photon reaching the sample and $C_j$ the charge generated per photon reaching the diode. A simplified relationship with a lookup table can be used$$\begin{equation}C_s(C) = \sum_i w_i \mathrm{LUT}(E_i)\end{equation}$$Finally in order to allow a fast read-out, current is converted to frequency by an oscillator$$\begin{equation}I(\text{Hz})=\frac{F_{\text{max}}(Hz)}{V_{\text{max}}(V)}\frac{V_{\text{max}}(V)}{I_{\text{max}}(A)}I(A)+F_{0}(Hz)\end{equation}$$where $F_{\text{max}}$ the maximal frequency that can be detected, $F_{0}$ a fixed offset, $V_{\text{max}}$ the maximal output voltage of the ammeter and input voltage of the oscillator, $\frac{V_{\text{max}}(V)}{I_{\text{max}}(A)}$ the "gain" of the ammeter. Sometimes $I_{\text{max}}(A)$ is referred to as the diode "gain". Absolute diode--------------An absolute diode has a spectral responsivity $R(A/W)$ which behaves as theoretically expected<jupyter_code>import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

from spectrocrunch.detectors import diode
det = diode.factory("sxm_ptb")
print(det)

det.model = False
energy = det.menergy
R = det.spectral_responsivity(energy)
plt.plot(energy,R,marker='o',linestyle="",label='measured')

det.model = True
energy = np.linspace(1,10,100)
R = det.spectral_responsivity(energy)
plt.plot(energy,R,label='model')

plt.legend()
plt.xlabel('Energy (keV)')
plt.ylabel('Spectral responsivity (A/W)')
plt.show()<jupyter_output>/data/id21/inhouse/wout/dev/virtualenvs/lid21data/spectrocrunch/py37/lib/python3.7/site-packages/numpy/core/_asarray.py:83: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  return array(a, dtype, copy=False, order=order)
/data/id21/inhouse/wout/dev/virtualenvs/lid21data/spectrocrunch/py37/lib/python3.7/site-packages/scipy/interpolate/interpolate.py:437: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  y = array(y, copy=self.copy)
/data/id21/inhouse/wout/dev/virtualenvs/lid21data/spectrocrunch/py37/lib/python3.7/site-packages/numpy/core/_asarray.py:83: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  return array(a, dtype, copy=False, order=order)
/data/id21/inhouse/wout/dev/virtualenvs/lid21data/spectrocrunch/py37/lib/python3.7/site-packages/numpy/core/_asarray.py:83: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  return array[...]<jupyter_text>Calibrated diode----------------The spectral responsivity $R(A/W)$ of a calibrated diode is determined by the difference in response with an absolute diode<jupyter_code>det = diode.factory("sxm_idet",npop=4)
print(det)

det.model = False
energy = det.menergy
R = det.spectral_responsivity(energy)
plt.plot(energy,R,marker='o',linestyle="",label='measured')

det.model = True
energy = np.linspace(1,12,100)
R = det.spectral_responsivity(energy)
plt.plot(energy,R,label='model')

det.model = False
R = det.spectral_responsivity(energy)
plt.plot(energy,R,label='used')

plt.legend(loc='best')
plt.xlabel('Energy (keV)')
plt.ylabel('Spectral responsivity (A/W)')
plt.show()<jupyter_output>/data/id21/inhouse/wout/dev/virtualenvs/lid21data/spectrocrunch/py37/lib/python3.7/site-packages/scipy/interpolate/interpolate.py:437: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  y = array(y, copy=self.copy)
/data/id21/inhouse/wout/dev/virtualenvs/lid21data/spectrocrunch/py37/lib/python3.7/site-packages/numpy/core/_asarray.py:83: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  return array(a, dtype, copy=False, order=order)
/data/id21/inhouse/wout/dev/virtualenvs/lid21data/spectrocrunch/py37/lib/python3.7/site-packages/numpy/core/_asarray.py:83: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  return array(a, dtype, copy=False, order=order)
/mntdirect/_data_id21_inhouse/wout/dev/SpectroCrunchDev/fluxnorm/spectrocrunch/detectors/diode.py:1754: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  energy[ind]
/data/id21/inhouse/w[...]<jupyter_text>Direct detection------------------When $Y_{i,j}$ only contains transmission factors (i.e. not fluorescence/scattering from a secondary target) the diode measures $I_s$ directly. The relationship between flux $I_s(Hz)$ and diode response $I(Hz)$ is determined by the spectral responsivity which should be known (absolute diode) or calibrated (with respect to an absolute diode):<jupyter_code>from spectrocrunch.optics import xray as xrayoptics

atmpath = xrayoptics.Filter(material="vacuum", thickness=1)
det = diode.factory("sxm_idet",optics=[atmpath])
print(det)

Is = 3e9
energy = np.linspace(2,10,100)

for gain in [1e7]:
    for model in [False,True]:
        det.gain = gain
        det.model = model
        I = det.fluxtocps(energy[:, np.newaxis], Is).to("Hz").magnitude
        plt.plot(energy,I,label="{:.0e} V/A{}".\
                 format(gain," (model)" if model else ""))

plt.gca().axhline(y=det.oscillator.Fmax.to("Hz").magnitude,label="Fmax",\
                  color='k',linestyle='--')

plt.title("Flux = {:.0e} Hz".format(Is))
plt.legend(loc='best')
plt.xlabel('Energy (keV)')
plt.ylabel('Response (Hz)')
plt.show()<jupyter_output>/data/id21/inhouse/wout/dev/virtualenvs/lid21data/spectrocrunch/py37/lib/python3.7/site-packages/scipy/interpolate/interpolate.py:437: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  y = array(y, copy=self.copy)
/mntdirect/_data_id21_inhouse/wout/dev/SpectroCrunchDev/fluxnorm/spectrocrunch/detectors/diode.py:1754: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  energy[ind]<jupyter_text>Indirect detection------------------ The conversion factors $Y_{i,j}$ can be calculated from the cross-sections of the secondary target.  Without opticsWhen the indirect diode measures the flux downstream from the optics (or there are no optics at all), the relationship between flux and measured count-rate is known (because $T_s$ is known):<jupyter_code>atmpath = xrayoptics.Filter(material="vacuum", thickness=1)
iodet = diode.factory("sxm_iodet1",optics=[atmpath])
iodet.gain = 1e9
print(iodet.geometry)

Is = 5e9
energy = np.linspace(1,10,50)
I = iodet.fluxtocps(energy[:, np.newaxis], Is).to("Hz").magnitude
plt.plot(energy,I)

plt.gca().axhline(y=iodet.oscillator.Fmax.to("Hz").magnitude,label="Fmax",\
                  color='k',linestyle='--')

plt.title("Flux = {:.0e} Hz".format(Is))
plt.xlabel('Energy (keV)')
plt.ylabel('Response (Hz)')
plt.show()<jupyter_output>XraySource:
 intensity = 0.9999999999999998 (W/m^2)
 dop = 1.0
 dolp = 1.0
 polangle = 0.0 (deg)
 handedness = none
PN-diode:
 Ionization energy = 3.6685605263157894 eV
 Material = Si (0.1 cm)
Beam filters: None
Detector filters: None
Secondary target:
 Multilayer (ordered top-bottom):
  Layer 0. 0.5 um (Ti)
  Layer 1. 0.5 um (silicon nitride)
Optics:
 Filter Multilayer (ordered top-bottom):
  Layer 0. 10000.0 um (vacuum)
Before sample: True
Ammeter:
 Gain = 1.000000e+09 V / A
 Dark current = 0.0 e / s
 Output voltage = 10 V
Voltage-to-Frequency:
 y hertz = 1.000000e+06 Hz/10 V * x V + 0 Hz
Geometry:
 In = 90 deg
 Out = 70 deg (reflection)
 Sample azimuth = 180.0 deg
 Detector azimuth = 0 deg
 Solid angle = 4*pi*0.4 srad<jupyter_text>With opticsIn case the indirect diode is upstream from the optics, transmission $T_s$ needs to be calibrated with a direct diode. This is done by measuring a changing flux at fixed energy, e.g. by scanning a series of attenuators. The flux is calculated from the direct diode and used to calibrate the response of the indirect diode:<jupyter_code>iodet = diode.factory("sxm_iodet1",optics=[atmpath, "KB"])
iodet.gain = 1e8

idet = diode.factory("sxm_idet")
idet.gain = 1e6

energy = 7
idetresp = np.linspace(3e4,5e4,100)
fluxmeas = idet.cpstoflux(energy,np.random.poisson(idetresp))

iodetresp = np.random.poisson(np.linspace(2e5,3e5,100))
fitinfo = iodet.calibrate(iodetresp,fluxmeas,energy,caliboption="optics")
print(iodet.geometry)

plt.plot(fluxmeas,iodetresp,marker='o',linestyle="")
plt.plot(fluxmeas,iodet.fluxtocps(energy,fluxmeas))
label = "\n".join(["{} = {}".format(k,v) for k,v in fitinfo.items()])
plt.annotate(label,xy=(0.5,0.1),xytext=(0.5,0.1),\
            xycoords="axes fraction",textcoords="axes fraction")

plt.title("Gain = {:~.0e}".format(iodet.gain))
plt.xlabel('Flux (Hz)')
plt.ylabel('Response (Hz)')
plt.show()<jupyter_output>/data/id21/inhouse/wout/dev/virtualenvs/lid21data/spectrocrunch/py37/lib/python3.7/site-packages/numpy/core/_asarray.py:83: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  return array(a, dtype, copy=False, order=order)
/data/id21/inhouse/wout/dev/virtualenvs/lid21data/spectrocrunch/py37/lib/python3.7/site-packages/scipy/interpolate/interpolate.py:437: UnitStrippedWarning: The unit of the quantity is stripped when downcasting to ndarray.
  y = array(y, copy=self.copy)<jupyter_text>Note that the slope is $C_s(C)$ (the charge generated per photon reaching the sample, expressed here in units of elementary charge) and the intercept $D(A)$ (the dark current of the diode).  Manual calibrationCalibration can also be done manually for a single flux-reponse pair. The response is expected to be $I(Hz)$ but it can also be $I(A)$. If you want a linear energy interpolation, calibration can also be simplified in which case it simply stores a lookup table for $C_s(C)$.<jupyter_code>#Specify quantities manually with units:
#from spectrocrunch.patch.pint import ureg
#current = ureg.Quantity(1e-8,"A")

for simple in [True, False]:
    iodet = diode.factory("sxm_iodet1",optics=[atmpath, "KB"],simplecalibration=simple)
    iodet.gain = 1e8

    # Calibrate with Hz-Hz pair
    cps = 100000
    flux = 1e9
    energy = 6

    iodet.calibrate(cps,flux,energy,caliboption="optics")
    current = iodet.fluxtocurrent(energy,flux)

    # Calibrate with A-Hz pair
    energy = 10
    current *= 0.5
    iodet.calibrate(current,flux,energy,caliboption="optics")
    label = "C$_s$ table" if simple else "Calibrated T$_s$"
    print(label)
    print(iodet)
    print("")

    energy = np.linspace(6,10,10)
    response = [iodet.fluxtocps(en,flux).magnitude for en in energy]
    plt.plot(energy,response,label=label)

plt.legend()
plt.title("Gain = {:~.0e}, Flux = {:.0e}".format(iodet.Rout,flux))
plt.xlabel('Energy (keV)')
plt.ylabel('Response (Hz)')

plt.show()<jupyter_output>C$_s$ table
PN-diode:
 Ionization energy = 3.6685605263157894 eV
 Material = Si (0.1 cm)
Beam filters: None
Detector filters: None
LUT (e-/sample photon):
 6.0 keV: 62.415090744607646 e
 10.0 keV: 31.207545372303823 e
Ammeter:
 Gain = 1.000000e+08 V / A
 Dark current = 0.0 e / s
 Output voltage = 10 V
Voltage-to-Frequency:
 y hertz = 1.000000e+06 Hz/10 V * x V + 0 Hz
{'fraction_non_alphanumeric': 0.1477936026149895, 'fraction_numerical': 0.03058603782395517, 'mean_word_length': 5.174915905814513, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '13397', 'license': 'MIT', 'n_tokens_mistral': 4964, 'n_tokens_neox': 4508, 'n_words': 1138, 'path': 'doc/source/tutorials/diodes.ipynb', 'repo_name': 'woutdenolf/spectrocrunch', 'size': '12849'}