Example Calculations of X-ray properties of materials

A few detailed examples of using the xraydb.sqlite to calculate the X-ray properties of materials are presented here. These all use the functions in the python xraydb module, which is describe in more detail in the next chapter, Using XrayDB from Python. The examples will explore some aspects of X-ray physics, but will not give a complete tutorial on the concepts here. For a good reference on X-ray physics, see [Als-Nielsen and McMorrow (2011)].

Many of these calculations are also available at XrayDB Web App (xrayabsorption.org).

X-ray attenuation by elements

The XrayDB database tabulates values of the X-ray mass attenuation coefficient, \(\mu/\rho\), for each element. In most of the X-ray regime used in materials characterization (say, up to 150 keV), the photo-electric effect is the main process that causes X-ray attenuation. When the photo-electric process is dominant, the values for \(\mu/\rho\) depends strongly on Z of the atom and on X-ray energy E. In addition to these strong dependencies, sharp increases – so-called absorption edges – with be see at energies of bound core electron levels of atoms. To illustrate these characteristics, the following script will plot \(\mu/\rho\) for selected elements:

#!/usr/bin/env python
# XrayDB example script python/examples/mu_elements.py
# plot X-ray mass attenuation for selected elements
import numpy as np
import matplotlib.pyplot as plt
import wxmplot.interactive as wi
from xraydb import mu_elam , atomic_symbol

energy = np.arange(500, 120000, 10)  # energy in eV
for elem in ('C', 'Cu', 'Au'):
    mu = mu_elam(elem, energy)
    plt.plot(energy, mu, label=elem, linewidth=2)
plt.title('X-ray mass attenuation')
plt.xlabel('Energy (eV)')
plt.ylabel(r'$\mu/\rho \rm\, (cm^2/gr)$')

X-ray mass attenuation coefficient for C, Cu, and Au.

As you can see in Figure from this figure, the attenuation drops very strongly with \(E\) – approximately as \(E^3\). \(\mu\) also depends strongly with Z, though the sharp absorption edges make this more complicated.

You can also observe that at relatively high energies for relatively low-Z elements (such as C above about 20 keV) that the attenuation levels off. This is because the coherent (Rayleigh) and incoherent (Compton) scattering processes dominate, so that the photo-electric absorption is no longer the dominant X-ray scattering process. This can be illustrated by plotting the different components of \(\mu/\rho\) for C, as with the following script:

#!/usr/bin/env python
# XrayDB example script python/examples/mu_components_C.py
# plot components of X-ray mass attenuation for C 
import numpy as np
import matplotlib.pyplot as plt
from xraydb import mu_elam 

energy = np.arange(500, 120000, 10)  # energy in eV
elem = 'C'
mu_total = mu_elam(elem, energy, kind='total')
mu_photo = mu_elam(elem, energy, kind='photo')
mu_incoh = mu_elam(elem, energy, kind='incoh')
mu_coher = mu_elam(elem, energy, kind='coh')

plt.title('X-ray mass attenuation for %s' % elem)
plt.plot(energy, mu_total, linewidth=2, label='Total')
plt.plot(energy, mu_photo, linewidth=2, label='Photo-electric')
plt.plot(energy, mu_incoh, linewidth=2, label='Incoherent')
plt.plot(energy, mu_coher, linewidth=2, label='Coherent')

plt.xlabel('Energy (eV)')
plt.ylabel(r'$\mu/\rho \rm\, (cm^2/gr)$')

which will generate the following plot:


X-ray scattering and attenuation factors for C.

Note that above 20 keV, the photo-electric absorption and incoherent Compton contributions are about equal, and that the Compton scattering dominates above 50 keV. As shown above, the photo-electric scattering will be much higher for heavier elements. The Rayleigh and Compton scattering have a much weaker dependence on Z, so that the photo-electric process dominates to higher energies. Replacing ‘C’ with ‘Fe’ in the script above will generate the following plot:


X-ray scattering and attenuation factors for Fe.

which shows that the Compton scattering reaching about 0.1 to 0.25 \(\rm cm^2/gr\) for Fe, about the same value as it was for C, while the photo-electric cross-section dominates past 100 keV.

\(\mu\) calculations for materials

While one can use the above values for \(\mu/\rho\) to calculate the attenuation of X-rays by multi-element materials, the material_mu() function is available to do the more convenient calculation of the X-ray absorption coefficient \(\mu\) in units of 1/cm for a material and energy value and density (which are known for several common materials). This gives the length for which X-ray intensity is reduced by a factor of e, and so can be used to calculate the fraction of the X-rays transmitted through a material of known thickness, as \(\exp(-t\mu)\) for a material of thickness t. As a first example, we calculate the the fraction of X-ray transmitted through 1 mm of the water as a function of X-ray energy:

#!/usr/bin/env python
# XrayDB example script    python/examples/mu_water.py
# calculate the fraction of X-rays transmitted through 1 mm of water
import numpy as np
import matplotlib.pyplot as plt

from xraydb import material_mu

energy = np.linspace(1000, 41000, 201)

mu = material_mu('H2O', energy)

# mu is returned in 1/cm
trans = np.exp(-0.1 * mu)

plt.plot(energy, trans, label='transmitted')
plt.plot(energy, 1-trans, label='attenuated')
plt.title('X-ray absorption by 1 mm of water')
plt.xlabel('Energy (eV)')
plt.ylabel('Transmitted / Attenuated fraction')

Fraction of X-rays absorbed and transmitted by water


mu = material_mu('H2O', energy)


mu = material_mu('CaCO3', energy, density=2.71)

would generate the following plot


Fraction of X-rays absorbed and transmitted by calcite

For many X-ray experiments, selecting the size of a material size so that its thickness is approximately 1 to 2 absorption length is convenient so that X-ray scattering and emission can be observed strongly, with neither all primary and scattered X-rays being absorbed by the material itself, but also not simply passing through the material without any interaction. For example, one can simply do:

>>> from xraydb import material_mu
>>> mu_20kev = xraydb.material_mu('CaCO3', 20000, density=2.71)
>>> print("CaCO3 1/e depth at 20keV = {:.3f} mm".format(10/mu_20kev))
CaCO3 1/e depth at 20keV = 0.648 mm

X-ray flux calculations for ionization chambers and photodiodes

Gas-filled ionization chambers are widely used as X-ray detectors. They are simple to use, inexpensive, and can be highly linear in estimating the photon flux over many orders of magnitude. X-rays entering a chamber filled with an inert gas (typically He, N2, or one of the noble gases, or a mixture of these) will be partially absorbed by the gas, with the strong energy dependence shown above. By adjusting the composition of the gas, nearly any fraction of the incident X-ray beam can be absorbed at a particular X-ray energy, making these ideal detectors to sample the intensity of an X-ray beam incident on a sample, while attenuating only a fraction of the beam.

Some of the X-rays in the gas will be absorbed by the photo-electric effect which will ionize the gas, generating free electrons and energetic ions. Te first ionization event will generate an electron-ion pair with the energy of the X-ray minus the binding energy of the core electron. The high-energy electron and ion pair will further ionize other gas molecules. With an electric potential (typically on the order of 1 kV /cm) across the plates of the chamber, a current can be measured that is proportional to the X-ray energy and fluence of the X-rays.

In addition to the photo-electric absorption, X-rays can be attenuated by gas molecules in an ion chamber by incoherent (Compton) or coherent (Rayleigh) scattering processes. The coherent scattering will not generate any electrons in the gas, but will elastically scatter X-rays out of the main beam. Incoherent scattering will generate some current, though not all (and typically only a small portion) of the incident X-ray energy is given to an electron to generate a current. Compton scattering gives a distribution of energies to the scattered electron depending on the angle of scattering. The median energy of electrons generated by Compton scattering X-rays of energy \(E\) at 90 degrees will be

\[E_{median} = E / (1 + m_ec^2 / E)\]

For X-rays of 10 keV, \(E_{median}\) is about 192 eV. For 20 keV X-rays, it will be 750 eV, and for 50 keV X-rays, it will be 4.5 keV. Because the angular distribution of Compton scattering is not uniform, these median values over-estimate the amount of energy transferred to the scattered electron by a small amount that increases with energy. The mean energy of the Compton-scattered electron can be found by integrating the Klein-Nishina distribution. Since these values depend only on the incident X-ray energy, these calculations have been done and the values tabulated in the Compton_energies table in the XrayDB sqlite database.

Although the energy transferred to the electron by Compton scattering is much less than by the photo-electric process the contribution can be important. This is especially true for low-Z gas molecules such as He and N2 at relatively high energies (10 keV and above) for which incoherent scattering becomes much more important than photo-electric absorption, as shown above for C. That is, for accurate estimates of fluxes from ion chamber currents at energies about 20 keV or so, the contribution from Compton scattering should be included. For photo-diodes (typically made of Si), the Compton scattering cross-section exceeds the photo-electric cross-section about 56 keV.

Effective Ionization Potentials of gases and semiconductors

The process of converting the X-ray generated current into X-ray fluence involves several steps. The energy from a single X-ray-generated electron is converted into a number of electron-ion pairs given by the effective ionization potential of the gas. These are reasonably well-known values (see [Knoll (2010)]) that are all between 20 and 40 eV, given in the Table of Effective Ionization Potentials.

Table of Effective Ionization Potentials. Many of these are taken from [Knoll (2010)], while others appear to come from International Commission on Radiation Units & Measurement, Report 31, 1979. The names given are those supported by the functions ionization_potential() and ionchamber_fluxes().

gas/materia name(s)

potential (eV)

hydrogen, H


helium, He


nitrogen, N, N2


oxygen, O, O2


neon, Ne


argon, Ar


krypton, Kr


xenon, Xe




methane, CH4


carbondioxide, CO2


silicon, Si


germanium, Ge


From this table, we can see that the absorption (by photo-electric effect) of 1 X-ray of energy 10 keV will eventually generate about 300 electron-ion pairs. That is not much current, but if \(10^8 \,\rm Hz\) X-rays are absorbed per second, then the current generated will be around 5 nA. Of course, the thickness of the gas or more precisely the length of gas under ionizing potential will have an impact on how much current is generated. The photo-current will then be amplified and converted to a voltage using a current amplifier, and that voltage will then recorded by a number of possible means. Note that while the ion chamber itself will be linear over many orders of magnitude of X-ray flux (provided the potential between the plates is high enough - typically in the 1 kV/cm range to efficiently collect all the charged particles before the recombine), a current amplifier at a particular setting of sensitivity will be linear only over a couple orders of magnitude (typically between output voltage of 0.05 to 5 V). Because of this, the sensitivity of the current amplifier used with an ion chamber needs careful attention.

A photo-diode works in much the same way as an ionization chamber. X-rays incident on the diode (typically Si or Ge) will be absorbed and generate a photo-current that can be collected. Typically PIN diodes are used, and with a small reverse bias voltage. Because the electrons do not need to escape the material but generate a current transported in the semiconductor, the effective ionization potential is much lower - a few times the semiconductor band gap instead of a few time the lowest core-level ionization potential. The current generated per X-ray will be larger than for an ion chamber, but still amplified with a current amplifier in the same way as is used for an ion chamber. Generally, diodes are thick enough that they absorb all incident X-rays.

Ion Chamber Flux calculations

The conversion of incident flux at a particular energy to generated current is not too difficult if considering only the photo-electric effect of a single gas, but can be somewhat subtle in the more general case. For the discussion here, we assume that the potential across the plates of the ion chamber is high enough to prevent any recombination of charged particles.

For a given gas at an incident X-ray energy \(E\), we calculate the total, photo-electric, incoherent (Compton), and coherent (Rayleigh) values of \(\mu\). If more than one gas is used, the weighted sum is calculated, so that we have \(\mu_{\rm total}\), \(\mu_{\rm photo}\), \(\mu_{\rm incoh}\), and \(\mu_{\rm coh}\) for the gas in the chamber or diode material.

The flux transmitted out of the chamber is

\[I_{\rm trans} = I_0 e^{-t \mu_{\rm total}}\]

where \(t\) is the length of the chamber and \(I_{0}\) is the incident flux. These two intensities are the quantity we are most interested in. The attenuated flux (in number per second, or Hz) is

\[I_{\rm atten} = I_0 (1 - e^{-t \mu_{\rm total}})\]

can be separated into the various source of attenuation as

\begin{eqnarray*} & I_{\rm photo} & = I_{\rm atten} \, \mu_{\rm photo} / \mu_{\rm total} \\ & I_{\rm incoh} & = I_{\rm atten} \, \mu_{\rm incoh} / \mu_{\rm total} \\ & I_{\rm coh} & = I_{\rm atten} \, \mu_{\rm coh} / \mu_{\rm total} \\ \end{eqnarray*}

The photo-electric effect converts all of the X-ray energy into a current of both electrons and ions using the effective ionization potential above:

\[C_{\rm photo} = 2 q_e E I_{\rm photo} / V_{\rm eff}\]

where \(q_{e}\) is the electron charge (1.6e-19 C), \(E\) is the incident X-ray energy (in eV), \(I_{\rm photo}\) is the flux (in Hz), and \(V_{\rm eff}\) is the effective ionization potential for the gas. The leading 2 comes because both electrons and ions are typically counted for the current from an ion chamber. It is sometimes useful to add a Frisch mesh grid to collect the slower ions and shunt them so as to not count that portion of the current, and thereby give the ion chamber a faster time response. In that case, the current will be half of the value given above.

The coherent (Rayleigh) scattering produces no electrons, but the incoherent (Compton) scattering does. The energy of the Compton-scattered electron varies with both X-ray energy and scattering angle, as does the probability of scattering. Integrating over all angles gives the mean electron energy, which we use to obtain the current from the incoherent scattering:

\[C_{\rm incoh} = 2 q_e E_{\rm mean} I_{\rm incoh} / V_{\rm eff}\]

where \(E_{\rm mean}\) is the mean energy of Compton-scattered electron (approximately, but slightly less than the \(E_{\rm median}\) value above.

The current from an ion_chamber is typically measured as a voltage generated by a current-to-voltage amplifier. The measured voltage will have a gain or sensitivity in units of A/V. The goal is typically to calculate the flux \(I_0\) from the measured voltage and knowledge of the sensitivity as well as the gas(es), ion chamber length \(t\), and X-ray energy \(E\). The measured voltage is given by

\[V = I_0 ( C_{\rm photo} + C_{\rm incoh} ) / S\]

where \(S\) is the amplifier sensitivity in A/V. From this, \(I_0\) and \(I_{\rm trans}\) can be calculated.


The function ionchamber_fluxes() will calculate X-ray fluxes for an ion chamber as described above the following inputs:

  • gas: the gas, or mixture of gases used or ‘Si’ or ‘Ge’ for diodes.

  • length: the length of the ion chamber, in cm.

  • energy: the X-ray energy, in eV.

  • volts: the output voltage of the current amplifier

  • sensitivity and sensitivity_units: the sensitivity or gain of the amplifier used to convert the photo-current to the recorded voltage.

  • with_compton: whether to include the current generated by Compton-scattered electrons [True]

  • both_carriers: whether to include the current generated by both positive and negative charged particles [True]

The default sensitivity_units is ‘A/V’ but can be set to any of the common SI prefixes such as ‘p’, ‘pico’, ‘n’, ‘nano’, \(\mu\), (unicode ‘03bc’), ‘u’, ‘micro’, ‘m’, or ‘milli’, so that:

>>> fluxes = ionchamber_fluxes('N2', volts=1, energy=10000, length=10,
>>> fluxes = ionchamber_fluxes('N2', volts=1, energy=10000, length=10,
                               sensitivity=1, sensitivity_units='nA/V')

will give the same results.

The output from ionchamber_fluxes() is a named tuple with 4 fields:

  • photo - the flux absorbed by the photo-electric effect, in Hz.

  • incoherent - the flux scattered by the Compton effects, in Hz.

  • incident - the flux incident on the ion chamber, in Hz.

  • transmitted - the flux beam leaving the ion chamber, in Hz.

As described above, the current in the ion chamber or photo-diode is generated by electrons and ions produced by both the photo-electric and incoherent or Compton scattering. The photo-electric cross-section will dominate for heavy elements and relatively low X-ray energies, but does not necessarily dominate at high X-ray energies. The photo-electric cross-section with the incident X-ray energy and the incoherent cross-section with the *mean* Compton-scattering energy, using the calculated and tabulated mean energies of the Compton-scattered electrons are used to estimate the incident flux from the photo-current. The total attenuation cross-section, including the coherent cross-sections, is used to calculate the transmitted flu from the incident flux.

As an example calculation of ion chamber currents:

>>> fl = ionchamber_fluxes(gas='nitrogen', volts=1.25, energy=18000,
                               length=10.0, sensitivity=1.e-6)
>>> print(f"Incident= {fl.incident:g} Hz, Transmitted flux= {fl.transmitted:g} Hz")
Incident= 2.2358e+12 Hz, Transmitted flux= 2.214e+12 Hz

It is not uncommon for an ion chamber to be filled with a mixture of 2 or more gases so as to better control the fraction of X-rays absorbed in a chamber of fixed length. This can be specified by passing in a dictionary of gas name and fractional density, as with:

>>> fl = ionchamber_fluxes(gas={'Kr':0.5, 'Ar': 0.5}, volts=1.25,
                           energy=18000, length=10,
                           sensitivity=1, sensitivity_units='microA/V')
>>> print(f"Incident= {fl.incident:g} Hz, Transmitted flux= {fl.transmitted:g} Hz")
Incident= 1.43737e+10 Hz, Transmitted flux= 3.28986e+09 Hz

Finally, the pressure of the gas is sometimes adjusted to alter the fraction of the beam absorbed. The calculations here all use the densities at STP, but changes in gas density will be exactly linear to changing the length of the ion chamber.

X-ray mirror reflectivities

At very shallow angles of incidence X-rays can be reflected by total external reflection from a material. The reflectivity can be very high at relatively low energies and shallow angles, but drops off dramatically with increasing energy, increasing angle, and decreasing electron density. Still, this reflectivity is one of the few ways to steer X-ray beams and so is widely used in synchrotron radiation sources.

The reflectivity can be calculated with the mirror_reflectivity() function which takes X-ray energy, incident angle, and mirror material as arguments.

An example script, comparing the energy-dependence of the reflectivity for a few common mirror materials is given as

import numpy as np
from xraydb import mirror_reflectivity
import matplotlib.pyplot as plt

energy = np.linspace(1000, 51000, 501)

r_si = mirror_reflectivity('Si', 0.002, energy)
r_ni = mirror_reflectivity('Ni', 0.002, energy)
r_rh = mirror_reflectivity('Rh', 0.002, energy)
r_pt = mirror_reflectivity('Pt', 0.002, energy)

plt.plot(energy, r_si, label='Si')
plt.plot(energy, r_ni, label='Ni')
plt.plot(energy, r_rh, label='Rh')
plt.plot(energy, r_pt, label='Pt')

plt.title('X-ray reflectivity at $\\theta=2 \mathrm{mrad}$')
plt.xlabel('Energy (eV)')

X-ray mirror reflectivity at \(\theta = 2\mathrm{mrad}\) for selected mirror surfaces and coatings used for mirrors.

Darwin widths of monochromator crystals

Bragg’s law describes X-ray diffraction from crystals as

\[m \lambda = 2 d \sin(\theta)\]

where \(\lambda\) is the X-ray wavelength, \(d\) the d-spacing of the crystal lattice plane, \(\theta\) the incident angle, and \(m\) the order of the reflection. For imperfect crystals, in which the lattice planes are not stacked perfectly over extended distances, the angular width of any particular reflection is dominated by the spread in d-spacing and the mosaicity inherent in the crystal. For perfect crystals, however, the angular width of a reflection is dominated by the fact that effectively all of the X-rays will scatter from the lattice well before any attenuation of the X-ray beam occurs. This dynamical diffraction gives a small but finite offset from the Bragg angle, and gives a broadened angular width to reflection. This is usually called the Darwin width (named for Charles G. Darwin, grandson of the more famous Charles R. Darwin). In addition, the refraction and in particular the absorption effects that give anomalous scattering (as calculated with xray_delta_beta()) make the “rocking curve” of reflected intensity as a function of angle an asymmetric shape.

All of these effects are included in the darwin_width() function, which follows very closely the description from chapter 6.4 in [Als-Nielsen and McMorrow (2011)]. The function takes inputs of

  • energy: the X-ray energy, in eV.

  • crystal: the atomic symbol for the crystal: ‘Si’, ‘Ge’, or ‘C’. [‘Si’]

  • hkl: a tuple with (h, k, l) of the reflection used. [(1, 1, 1)]

  • a: lattice constant [None - use nominal value for crystal]

  • polarization: s, p, or u to specify the X-ray polarization relative to the crystal [s]

  • m: the order of the reflection. [1]

  • ignore_f1: whether to ignore f1. [False]

  • ignore_f2: whether to ignore f2. [False]

Polarization of s should be used for vertically deflecting monochromators at most synchrotron sources (which will normally be horizontally polarized), and p should be used for horizontally deflecting monochromators. For crystals used to analyzed unpolarized X-ray emission, use u, which will give the average of s and p polarization.

As with ionchamber_fluxes(), the output here is complicated enough that it is put into a named DarwinWidth tuple that will contain the following fields:

  • theta - the nominal Bragg angle, in rad

  • theta_offset - the offset from the nominal Bragg angle, in rad.

  • theta_width - estimated angular Darwin width, in rad

  • theta_fwhm - estimated FWHM of the angular reflectivity curve, in rad

  • energy_width - estimated energy Darwin width, in eV

  • energy_fwhm - estimated FWHM energy reflectivity curve, in eV

  • zeta - nd-array of \(\zeta = \Delta\lambda/\lambda\).

  • dtheta - nd-array of angles around from Bragg angle, in rad

  • denergy - nd-array of energies around from Bragg energy, in eV

  • intensity - nd-array of reflected intensity at zeta values.

Here, dtheta will be given by \(\Delta\theta = \zeta \tan(\theta)\), and denergy will be given by \(\Delta{E} = \zeta E\). All of the nd-arrays will be the same size, so that plots of reflectivity can be readily made. An example usage, printing the predicted energy and angular widths and plotting the intensity profile or “rocking curve” is

import numpy as np
from xraydb import darwin_width
import matplotlib.pyplot as plt

dw_si111 = darwin_width(10000, 'Si', (1, 1, 1))
dw_si333 = darwin_width(30000, 'Si', (3, 3, 3))

fmt_string = "Darwin Width for {:s} at {:.0f} keV: {:5.2f} microrad, {:5.2f} eV"
print(fmt_string.format('Si(111)', 10,

print(fmt_string.format('Si(333)', 30,

dtheta  = dw_si111.dtheta*1e6
denergy = dw_si111.denergy[::-1]

#  slightly advanced matplotlib hackery:
fig, ax = plt.subplots(constrained_layout=True)

ax.plot(dtheta, dw_si111.intensity, label='$I$, Si(111)', linewidth=2)
ax.plot(dtheta, dw_si111.intensity**2, label='$I^2$, Si(111)', linewidth=2)
ax.plot(dw_si333.dtheta*1e6, dw_si333.intensity**2, label='$I^2$ Si(333) 30 keV', linewidth=2)

ax.set_title('X-ray diffraction intensity at 10keV')
ax.set_xlabel('Angle - $\\theta_B$ ($ \mu \mathrm{rad}$)')


which will print out values of:

Darwin Width for Si(111) at 10 keV: 26.96 microrad,  1.34 eV
Darwin Width for Si(333) at 30 keV:  1.81 microrad,  0.27 eV

and generates a plot of


X-ray monochromator diffracted intensities around the Si(111) reflection. Here, \(i\) represents the intensity of a single reflection, and \(i^2\) the intensity from 2 bounces, as for a double-crystal monochromator. The intensity and angular offset of the third harmonic is also shown.

Note that the values reported for theta_fwhm and energy_fwhm will be about 6% larger than the reported values for theta_width and energy_width. The width values closely follow the region of the curve where the reflectivity ignoring absorption would be 1 - the flat top of the curve. Since a double-crystal monochromator will suppress the tails of the reflectivity, this smaller value is the one typically reported as “the Darwin width”, though some sources will report this smaller value as “FWHM”.