import numpy as np
import pytest
import astropy.units as u
import roentgen
from roentgen.absorption.material import MassAttenuationCoefficient, Material
from roentgen.util import get_material_density, is_an_element
# elements beyond z = 93 have no mass absorption data
all_materials = list(roentgen.elements["symbol"])[:-6] + list(roentgen.compounds["symbol"])
energy_array = u.Quantity(np.arange(1, 100, 1), "keV")
[docs]
@pytest.fixture(params=all_materials)
def mass_atten(request):
return MassAttenuationCoefficient(request.param)
[docs]
def test_is_an_element_symbol():
"""Test that function is okay with all known elements symbols"""
for el in roentgen.elements["symbol"]:
assert is_an_element(el)
[docs]
def test_is_an_element_caseinsensitive_symbol():
"""Test that searching for an element symbol is not case sensitive"""
for el in roentgen.elements["symbol"]:
assert is_an_element(el.upper())
assert is_an_element(el.lower())
assert is_an_element(el.capitalize())
[docs]
def test_is_an_element_name():
"""Test that function is okay with all known elements names"""
for el in roentgen.elements["name"]:
assert is_an_element(el)
[docs]
def test_is_an_element_caseinsensitive_name():
"""Test that searching for an element name is not case sensitive"""
for el in roentgen.elements["name"]:
assert is_an_element(el.upper())
assert is_an_element(el.lower())
assert is_an_element(el.capitalize())
[docs]
def test_mass_atten(mass_atten):
# check that all materials can load a MassAttenuationCoefficient object
assert isinstance(mass_atten, MassAttenuationCoefficient)
[docs]
def test_returns_quantity(mass_atten):
assert isinstance(mass_atten.func(2 * u.keV), u.Quantity)
[docs]
def test_number_of_energies(mass_atten):
energy = u.Quantity(np.arange(2, 1000), "keV")
atten = mass_atten.func(energy)
assert len(energy) == len(atten)
[docs]
@pytest.fixture(params=all_materials)
def material(request):
return Material(request.param, 500 * u.micron)
[docs]
def test_material(material):
assert isinstance(material, Material)
[docs]
def test_material_give_density():
assert isinstance(
Material(all_materials[40], 500 * u.micron, density=1 * u.kg / u.m**3), Material
)
[docs]
def test_bad_add_to_material():
with pytest.raises(TypeError):
Material("Ge", 500 * u.micron) + ["foo", "bar"]
[docs]
@pytest.fixture(params=all_materials)
def thick_material(request):
return Material(request.param, 500 * u.Mm)
[docs]
def test_opaque(thick_material):
# check that extremely large amounts of material mean no transmission
assert thick_material.transmission(2 * u.keV) < 1e-6
[docs]
@pytest.fixture(params=all_materials)
def thin_material(request):
return Material(request.param, 1e-6 * u.pm)
[docs]
def test_transparent(thin_material):
# check that extremely large amounts of material mean no transmission
assert thin_material.transmission(2 * u.keV) > 0.90
[docs]
@pytest.mark.parametrize(
"material,energy,thickness",
[
("Si", 10 * u.keV, 1 * u.mm),
("Au", 1 * u.keV, 1 * u.micron),
("Air (dry)", 1 * u.keV, 1 * u.m),
("blood", 5 * u.keV, 1 * u.mm),
],
)
def test_linear_attenuation_coefficient(material, energy, thickness):
mat = Material(material, thickness)
assert np.isclose(
mat.linear_attenuation_coefficient(energy),
mat.mass_attenuation_coefficient(energy) * get_material_density(material),
rtol=1e-4,
)
[docs]
@pytest.mark.parametrize(
"material",
[
("Si"),
("Au"),
("Air (dry)"),
("blood"),
({"Fe": 0.98, "C": 0.02}), # steel
({"Cu": 0.88, "Sn": 0.12}), # bronze
({"water": 0.97, "Na": 0.015, "Cl": 0.015}), # salt water
],
)
def test_material_number_of_energies(material):
mat = Material(material, 1 * u.m)
energy = u.Quantity(np.arange(1, 1000), "keV")
assert len(mat.transmission(energy)) == len(energy)
assert len(mat.absorption(energy)) == len(energy)
assert len(mat.mass_attenuation_coefficient(energy)) == len(energy)
assert len(mat.linear_attenuation_coefficient(energy)) == len(energy)
[docs]
@pytest.mark.parametrize(
"material",
[
("Si"),
("Au"),
("Air (dry)"),
("blood"),
({"Fe": 0.98, "C": 0.02}), # steel
({"Cu": 0.88, "Sn": 0.12}), # bronze
({"water": 0.97, "Na": 0.015, "Cl": 0.015}), # salt water
],
)
def test_material_scalar_energy(material):
"""If a scalar energy is given then the results should NOT be arrays"""
mat = Material(material, 1 * u.m)
energy = 1 * u.keV
assert isinstance(mat.transmission(energy), float)
assert isinstance(mat.absorption(energy), float)
assert mat.mass_attenuation_coefficient(energy).isscalar
assert mat.linear_attenuation_coefficient(energy).isscalar
[docs]
@pytest.mark.parametrize(
"material",
[
("Si"),
("Au"),
("Air (dry)"),
("blood"),
({"Fe": 0.98, "C": 0.02}), # steel
({"Cu": 0.88, "Sn": 0.12}), # bronze
({"water": 0.97, "Na": 0.015, "Cl": 0.015}), # salt water
],
)
def test_mass_atten_calculation_in_material(material):
mat = Material(material, 1 * u.m)
energy = u.Quantity(np.arange(1, 1000), "keV")
result1 = mat.mass_attenuation_coefficient(energy)[10]
result2 = np.average(
u.Quantity(
[this_atten.func(energy[10]) for this_atten in mat.mass_attenuation_coefficients]
),
weights=mat.fractional_masses,
)
assert np.isclose(result1, result2)
[docs]
def test_density_calculation():
a = {"Fe": 0.98, "C": 0.02}
this_mat = Material(a, 5 * u.m)
calc_density = get_material_density("Fe") * 0.98 + get_material_density("C") * 0.02
assert np.isclose(this_mat.density, calc_density)
a = {"Fe": 0.5, "C": 0.5}
this_mat = Material(a, 5 * u.m)
calc_density = get_material_density("Fe") * 0.5 + get_material_density("C") * 0.5
assert np.isclose(this_mat.density, calc_density)
a = {"Fe": 1, "C": 1}
this_mat = Material(a, 5 * u.m)
calc_density = get_material_density("Fe") * 0.5 + get_material_density("C") * 0.5
assert np.isclose(this_mat.density, calc_density)
[docs]
def test_raise_outside_of_data_range():
"""Test that ValueError is raised is trying to get values outside of data range 1 keV to 20 MeV."""
mat = Material("Fe", 1 * u.m)
energy = u.Quantity(np.arange(0.1, 10, 0.1), "keV")
# below 1 keV
with pytest.raises(ValueError):
mat.absorption(energy)
with pytest.raises(ValueError):
mat.transmission(energy)
# above 20 MeV
energy = u.Quantity(np.arange(10, 23, 0.1), "MeV")
with pytest.raises(ValueError):
mat.absorption(energy)
with pytest.raises(ValueError):
mat.transmission(energy)
[docs]
def test_repr_str():
a = {"Fe": 0.98, "C": 0.02}
this_mat = Material(a, 5 * u.m)
assert isinstance(this_mat.__repr__(), str)
assert isinstance(this_mat.__str__(), str)