Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6333d02
Initial commit of transmission conversions
Matt-Carre May 5, 2026
1040280
added docstrings
Matt-Carre May 5, 2026
ef61984
initial commit
Matt-Carre May 5, 2026
7e8a3f5
adds pydantic validation
Matt-Carre May 5, 2026
64c844c
Added pydantic validation
Matt-Carre May 5, 2026
4d48090
changes to NonNegativeFloat (accepting 0) where relevant
Matt-Carre May 5, 2026
da51855
changes to NonNegativeFloat (accepting 0) where relevant, considering…
Matt-Carre May 5, 2026
e5c341d
fixes linting. going to move tests next update.
Matt-Carre May 5, 2026
5215f96
labels tests
Matt-Carre May 6, 2026
3d6a920
Merge branch '2040_attenuation_transmission' into 2041_interconversio…
Matt-Carre May 6, 2026
273cbff
added test labels
Matt-Carre May 6, 2026
c9ef824
split tests
Matt-Carre May 6, 2026
7d94b52
split tests
Matt-Carre May 6, 2026
c32281d
merged in 2040
Matt-Carre May 6, 2026
10f65f8
adds specific output
Matt-Carre May 6, 2026
1a01d56
Merge branch 'main' into 2041_interconversion_depth_attenuation
Matt-Carre May 11, 2026
8896dd1
Merge branch 'main' into 2040_attenuation_transmission
Matt-Carre May 11, 2026
9c5f36b
Merge branch 'main' into 2041_interconversion_depth_attenuation
Matt-Carre May 13, 2026
a4fed4d
Merge branch 'main' into 2040_attenuation_transmission
Matt-Carre May 13, 2026
d4edcc1
Merge branch 'main' into 2041_interconversion_depth_attenuation
Matt-Carre May 13, 2026
0e3b3a9
Merge branch 'main' into 2040_attenuation_transmission
Matt-Carre May 13, 2026
1651348
Merge branch '2040_attenuation_transmission' of github.com:DiamondLig…
Matt-Carre May 13, 2026
c243228
rejoins tests
Matt-Carre May 13, 2026
3516fcb
Merge branch '2040_attenuation_transmission' into 2041_interconversio…
Matt-Carre May 13, 2026
788ad00
rejoined tests
Matt-Carre May 13, 2026
7763449
Merge branch '2041_interconversion_depth_attenuation' of github.com:D…
Matt-Carre May 13, 2026
3ce2471
updates based on review
Matt-Carre May 13, 2026
f1c30c5
renames test
Matt-Carre May 13, 2026
1fd5e87
fixed more comments
Matt-Carre May 13, 2026
7aa9941
fixed more comments
Matt-Carre May 13, 2026
b5e6663
adds bools to bad input
Matt-Carre May 13, 2026
28fce46
fixed based on comments in 2041
Matt-Carre May 13, 2026
a706c3a
fixes spelling mistake
Matt-Carre May 13, 2026
a20395e
changes name
Matt-Carre May 13, 2026
d0874bf
Merge branch '2041_interconversion_depth_attenuation' into 2040_atten…
Matt-Carre May 13, 2026
8b1f713
Merge branch 'main' into 2041_interconversion_depth_attenuation
Matt-Carre May 13, 2026
fbbd278
alters to make sure bool is rejected
Matt-Carre May 13, 2026
99ae8fa
Merge branch '2041_interconversion_depth_attenuation' of github.com:D…
Matt-Carre May 13, 2026
72389b2
accidentally deleted files, recovered. also ensures strictness in tra…
Matt-Carre May 13, 2026
8be2371
work in progress. need to create a basemodel
Matt-Carre May 13, 2026
60004d6
need to fix up non-negative floats but otherwise all up to date
Matt-Carre May 13, 2026
7b3c237
adds a base model to check an input value. Changed back to strictfloa…
Matt-Carre May 14, 2026
d32b52b
Merge branch 'main' into 2041_interconversion_depth_attenuation
Matt-Carre May 14, 2026
2d9a79d
Merge branch 'main' into 2041_interconversion_depth_attenuation
Matt-Carre May 14, 2026
b12b936
Merge branch 'main' into 2041_interconversion_depth_attenuation
Matt-Carre May 15, 2026
26e0258
Merge branch 'main' into 2041_interconversion_depth_attenuation
Matt-Carre May 15, 2026
68566cd
Merge branch 'main' into 2041_interconversion_depth_attenuation
Matt-Carre May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 77 additions & 5 deletions src/dodal/common/general_maths/material_absorption_maths.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
from pydantic import StrictFloat, validate_call
from typing import Annotated

from pydantic import (
BaseModel,
Field,
StrictFloat,
validate_call,
)

from dodal.common.general_maths.transmission_interconversion import (
attenuation_from_natural_log_of_transmission,
natural_log_of_transmission_from_attenuation,
)


class ValueCheck(BaseModel):
value: Annotated[StrictFloat, Field(ge=0)]


@validate_call
Expand All @@ -11,12 +27,68 @@ def photon_mass_attenuation_per_unit_length(

Args:
energy_kev (StrictFloat): energy
photon_absorption_factor_per_unit_length (StrictFloat): photon absorption factor per
unit length
photon_absorption_factor_per_unit_length (StrictFloat): photon absorption factor
per unit length
energy_dependence_exponent (StrictFloat): energy dependence exponent

Returns:
(float): mass attenuation per unit length.
"""
roll_off = energy_kev**energy_dependence_exponent
return photon_absorption_factor_per_unit_length * roll_off
return photon_absorption_factor_per_unit_length * (
energy_kev**energy_dependence_exponent
)


@validate_call
def attenuation_at_depth_cm(
depth_cm: StrictFloat, absorption_coefficient_per_cm: StrictFloat
) -> float:
"""Calculates attenuation in Barnett units, where 1000 Bn equivalent to 1/e,
0Bn to 1 and 2000 Bn to 1/(e^2).

Args:
depth_cm (StrictFloat): depth of absorption
absorption_coefficient_per_cm (StrictFloat): absorption coefficient per cm

Raises:
ValueError: If either depth_cm or absorption_coefficient are negative, an error
is raised

Returns:
(float): attenuation in Barnett units
"""
ValueCheck.model_validate({"value": depth_cm}, strict=True)
ValueCheck.model_validate({"value": absorption_coefficient_per_cm}, strict=True)
ln_t = -(depth_cm * absorption_coefficient_per_cm)
return attenuation_from_natural_log_of_transmission(ln_t)


@validate_call
def thickness_cm_required_to_attenuate(
target_attenuation_bn: StrictFloat,
absorption_coefficient_per_cm: StrictFloat,
) -> float:
"""Calculates material depth in cm.

Args:
target_attenuation_bn (StrictFloat): Target attenuation to meet in Barnett
attenuation units.
absorption_coefficient_per_cm (StrictFloat): absorption coefficient per cm


Raises:
ValueError: if attenuation is below zero, or absorption is below the minimum
meaningful absorption coefficient, a value error is raised

Returns:
(float): material depth in cm.
"""
minimum_meaningful_absorption_coefficient = 1.0e-14
Comment thread
Matt-Carre marked this conversation as resolved.
if absorption_coefficient_per_cm < minimum_meaningful_absorption_coefficient:
raise ValueError(
"Invalid absorption - this calculator is not for transparent media nor thos\
e with optical gain."
)
ValueCheck.model_validate({"value": target_attenuation_bn}, strict=True)
ln_target_t = natural_log_of_transmission_from_attenuation(target_attenuation_bn)
return -(ln_target_t / absorption_coefficient_per_cm)
96 changes: 95 additions & 1 deletion tests/common/general_maths/test_material_absorption_maths.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from pydantic import ValidationError

from dodal.common.general_maths.material_absorption_maths import (
attenuation_at_depth_cm,
photon_mass_attenuation_per_unit_length,
thickness_cm_required_to_attenuate,
)


Expand All @@ -27,7 +29,48 @@ def test_photon_mass_attenuation_per_unit_length(
):
assert photon_mass_attenuation_per_unit_length(
energy_kev, photon_absorption_factor_per_unit_length, energy_dependence_exponent
) == pytest.approx(result, 5.0e-8)
) == pytest.approx(result)


@pytest.mark.parametrize(
"target_attenuation_bn,absorption_coefficient_per_cm,result",
[
(0, 2.4, 0), # tests attenuator thickness required for transparency is zero
(
248.461,
2.13,
0.1166483568,
), # tests attenuator thickness frequired for arbitrary attenuation
],
)
def test_thickness_cm_required_to_attenuate(
target_attenuation_bn, absorption_coefficient_per_cm, result
):
assert thickness_cm_required_to_attenuate(
target_attenuation_bn, absorption_coefficient_per_cm
) == pytest.approx(result, rel=1e-6)


@pytest.mark.parametrize(
"depth_cm,absorption_coefficient_per_cm,result",
[
(
0.5,
2,
1000,
), # tests attenuation is 1 kilobarnett at single attenuation length
(
1.89,
0.316,
597.24,
), # tests attenuation matches expectations at arbitrary attenuation depth
(0.0, 2.5, 0), # tests attenuation is zero after zero depth
],
)
def test_attenuation_at_depth_cm(depth_cm, absorption_coefficient_per_cm, result):
assert attenuation_at_depth_cm(
depth_cm, absorption_coefficient_per_cm
) == pytest.approx(result, rel=1e-6)


# inauspicious path
Expand All @@ -49,3 +92,54 @@ def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_exponent(
):
with pytest.raises(ValidationError):
photon_mass_attenuation_per_unit_length(3500.0, 1.0, bad_input)


def test_thickness_cm_required_to_attenuate_with_transparent_medium():
with pytest.raises(ValueError):
transparent_medium = 1.0e-15
thickness_cm_required_to_attenuate(3500.0, transparent_medium)


def test_thickness_required_to_attenuate_raises_error_for_gain():
with pytest.raises(ValidationError):
thickness_cm_required_to_attenuate(-1, 1)


@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False])
def test_thickness_required_to_attenuate_raises_error_with_invalid_target_attenuation(
bad_input,
):
with pytest.raises(ValidationError):
thickness_cm_required_to_attenuate(bad_input, 1.0)


@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False])
def test_thickness_required_to_attenuate_raises_error_with_invalid_absorption(
bad_input,
):
with pytest.raises(ValidationError):
thickness_cm_required_to_attenuate(1.0, bad_input)


@pytest.mark.parametrize("bad_input", [-1, -5, -0.1])
def test_attenuation_at_depth_raises_error_with_invalid_absorption(bad_input):
with pytest.raises(ValidationError):
attenuation_at_depth_cm(1.0, bad_input)


@pytest.mark.parametrize("bad_input", [-1, -5, -0.1])
def test_attenuation_at_depth_raises_error_for_unphysical_depths(bad_input):
with pytest.raises(ValidationError):
attenuation_at_depth_cm(bad_input, 1.0)


@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False])
def test_attenuation_at_depth_raises_error_with_invalid_depth(bad_input):
with pytest.raises(ValidationError):
attenuation_at_depth_cm(bad_input, 1.0)


@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False])
def test_attenuation_at_depth_raises_error_with_invalid_attenuation(bad_input):
with pytest.raises(ValidationError):
attenuation_at_depth_cm(1.0, bad_input)
Loading