From 6333d02d60213a34db282bdf30f4d62506722253 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Tue, 5 May 2026 11:56:26 +0100 Subject: [PATCH 01/28] Initial commit of transmission conversions --- src/dodal/common/general_maths/__init__.py | 0 .../transmission_interconversion.py | 22 ++++++ tests/common/general_maths/__init__.py | 0 .../test_transmission_interconversion.py | 76 +++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 src/dodal/common/general_maths/__init__.py create mode 100644 src/dodal/common/general_maths/transmission_interconversion.py create mode 100644 tests/common/general_maths/__init__.py create mode 100644 tests/common/general_maths/test_transmission_interconversion.py diff --git a/src/dodal/common/general_maths/__init__.py b/src/dodal/common/general_maths/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/dodal/common/general_maths/transmission_interconversion.py b/src/dodal/common/general_maths/transmission_interconversion.py new file mode 100644 index 00000000000..c9c6711f117 --- /dev/null +++ b/src/dodal/common/general_maths/transmission_interconversion.py @@ -0,0 +1,22 @@ +import math + +canonical_barnett_conversion = -1.0e3 +reverse_barnett_conversion = -1.0e-3 + + +def attenuation_from_natural_log_of_transmission(ln_t: float): + return canonical_barnett_conversion * ln_t + + +def attenuation_from_transmission(transmission_as_fraction: float): + ln_t = math.log(transmission_as_fraction) + return attenuation_from_natural_log_of_transmission(ln_t) + + +def natural_log_of_transmission_from_attenuation(attenuation_bn: float): + return reverse_barnett_conversion * attenuation_bn + + +def transmission_from_attenutation(attenuation_bn: float): + ln_t = natural_log_of_transmission_from_attenuation(attenuation_bn) + return math.exp(ln_t) diff --git a/tests/common/general_maths/__init__.py b/tests/common/general_maths/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/common/general_maths/test_transmission_interconversion.py b/tests/common/general_maths/test_transmission_interconversion.py new file mode 100644 index 00000000000..cb1a376a167 --- /dev/null +++ b/tests/common/general_maths/test_transmission_interconversion.py @@ -0,0 +1,76 @@ +import math + +import pytest + +from dodal.common.general_maths import transmission_interconversion + + +# happy: +@pytest.mark.parametrize( + "ln_t,result", [(-1, 1000), (0, 0), (-0.4367, 436.7), (-5.9017, 5901.7)] +) +def test_attenuation_from_natural_log_of_transmission(ln_t, result): + assert transmission_interconversion.attenuation_from_natural_log_of_transmission( + ln_t + ) == pytest.approx(result) + + +@pytest.mark.parametrize( + "transmission_as_fraction,result", + [(1, 0), (0.37, 994.25227334), (0.871, 138.1133), (4.2e-4, 7775.2558)], +) +def test_attenuation_from_transmission(transmission_as_fraction, result): + assert transmission_interconversion.attenuation_from_transmission( + transmission_as_fraction + ) == pytest.approx(result) + + +@pytest.mark.parametrize( + "attenuation_bn,result", + [(0, 0), (1e3, -1), (712.6, -0.7126), (8034.1, -8.0341)], +) +def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): + assert transmission_interconversion.natural_log_of_transmission_from_attenuation( + attenuation_bn + ) == pytest.approx(result) + + +@pytest.mark.parametrize( + "attenuation_bn,result", + [(0, 1), (1e3, 0.3678794), (145.1, 0.8649358), (7221.9, 7.3041331435179e-4)], +) +def test_transmission_from_attenutation(attenuation_bn, result): + assert transmission_interconversion.transmission_from_attenutation( + attenuation_bn + ) == pytest.approx(result) + + +# inauspicious: + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_from_natural_log_of_transmission_errors(bad_input): + with pytest.raises(TypeError): + transmission_interconversion.attenuation_from_natural_log_of_transmission( + bad_input + ) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_from_transmission_errors(bad_input): + with pytest.raises(TypeError): + transmission_interconversion.attenuation_from_transmission(bad_input) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_natural_log_of_transmission_from_attenuation_errors(bad_input): + with pytest.raises(TypeError): + transmission_interconversion.natural_log_of_transmission_from_attenuation( + bad_input + ) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_transmission_from_attenutation_errors(bad_input): + with pytest.raises(TypeError): + transmission_interconversion.transmission_from_attenutation(bad_input) From 104028080a19ad2e8dee0da98620f57cce1d0f96 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Tue, 5 May 2026 12:09:27 +0100 Subject: [PATCH 02/28] added docstrings --- .../transmission_interconversion.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/dodal/common/general_maths/transmission_interconversion.py b/src/dodal/common/general_maths/transmission_interconversion.py index c9c6711f117..e2efaba9cb0 100644 --- a/src/dodal/common/general_maths/transmission_interconversion.py +++ b/src/dodal/common/general_maths/transmission_interconversion.py @@ -5,18 +5,52 @@ def attenuation_from_natural_log_of_transmission(ln_t: float): + """Converts from natural log of transmission fraction into Barnett attenuation units + . + + Args: + ln_t (float): natural log of transmission fraction + + Returns: + (float): Barnett attenuation units + """ return canonical_barnett_conversion * ln_t def attenuation_from_transmission(transmission_as_fraction: float): + """Converts from transmission fraction into Barnett attenuation units. + + Args: + transmission_as_fraction (float): transmission fraction + + Returns: + (float): Barnett attenuation units. + """ ln_t = math.log(transmission_as_fraction) return attenuation_from_natural_log_of_transmission(ln_t) def natural_log_of_transmission_from_attenuation(attenuation_bn: float): + """Converts from Barnett attenuation units into natural log of transmission fraction + . + + Args: + attenuation_bn (float): Barnett attenuation units + + Returns: + (float): natural log of transmission fraction + """ return reverse_barnett_conversion * attenuation_bn def transmission_from_attenutation(attenuation_bn: float): + """Converts from Barnett attenuation units into transmission fraction. + + Args: + attenuation_bn (float): Barnett attenuation units + + Returns: + (float): transmission fraction + """ ln_t = natural_log_of_transmission_from_attenuation(attenuation_bn) return math.exp(ln_t) From ef6198461297d12292b608d4dc188c5c376a069e Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Tue, 5 May 2026 14:14:21 +0100 Subject: [PATCH 03/28] initial commit --- .../material_absorption_maths.py | 85 ++++++++++++ .../test_material_absorption_maths.py | 123 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 src/dodal/common/general_maths/material_absorption_maths.py create mode 100644 tests/common/general_maths/test_material_absorption_maths.py diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py new file mode 100644 index 00000000000..12bd3df206f --- /dev/null +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -0,0 +1,85 @@ +from dodal.common.general_maths import transmission_interconversion + + +def photon_mass_attenuation_per_unit_length( + energy_kev: float, + photon_absorption_factor_per_unit_length: float, + energy_dependence_exponent: float, +): + """Calculates mass attenuation per unit length. + + Args: + energy_kev (float): energy + photon_absorption_factor_per_unit_length (float): photon absorption factor per + unit length + energy_dependence_exponent (float): energy dependence exponent + + Returns: + (float): mass attenuation per unit length. + """ + return photon_absorption_factor_per_unit_length * ( + energy_kev**energy_dependence_exponent + ) + + +def attenuation_at_depth_cm(depth_cm: float, absorption_coefficient_per_cm: float): + # TODO: check this - not sure if it really is the depth of the wedge? + """Calculates attenuation in Barnett units. + + Args: + depth_cm (float): depth of wedge + absorption_coefficient_per_cm (float): absorption coefficient per cm + + Raises: + ValueError: If either input value for either depth_cm or absorption_coefficient_ + per_cm <0.0, raises a value error. + + Returns: + (float): attenuation in Barnett units + """ + if depth_cm < 0.0: + raise ValueError(f"Negative depth is an invalid input: {depth_cm}") + if absorption_coefficient_per_cm < 0.0: + raise ValueError( + "Invalid absorption, this calculator is no for systems with\ + optical gain." + ) + return transmission_interconversion.attenuation_from_natural_log_of_transmission( + -(depth_cm * absorption_coefficient_per_cm) + ) + + +def thickness_cm_required_to_attenuate( + target_attenuation_bn: float, + absorption_coefficient_per_cm: float, +): + """Calculates material depth in cm. + + Args: + target_attenuation_bn (float): Target attenuation to meet in Barnett attenuation + units. + absorption_coefficient_per_cm (float): 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 + if ( + target_attenuation_bn < 0.0 + or 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." + ) + ln_target_t = ( + transmission_interconversion.natural_log_of_transmission_from_attenuation( + target_attenuation_bn + ) + ) + return -(ln_target_t / absorption_coefficient_per_cm) diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py new file mode 100644 index 00000000000..9d5b5695813 --- /dev/null +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -0,0 +1,123 @@ +import math + +import pytest + +from dodal.common.general_maths import material_absorption_maths + + +# happy path +@pytest.mark.parametrize( + "energy_kev,photon_absorption_factor_per_unit_length,energy_dependence_exponent," + "result", + [ + (5.042, 1.98e2, -2.717, 2.44170544), # Arbitrary + (8.3328, 2.5706e3, -2.83, 6.3708311), # Nickel + (11.9187, 1.48e3, -2.93, 1.03970725), # Gold-Three + (25.514, 6.48e3, -2.41, 2.63778077), # Silver + ], +) +def test_photon_mass_attenuation_per_unit_length( + energy_kev, + photon_absorption_factor_per_unit_length, + energy_dependence_exponent, + result, +): + assert material_absorption_maths.photon_mass_attenuation_per_unit_length( + energy_kev, photon_absorption_factor_per_unit_length, energy_dependence_exponent + ) == pytest.approx(result) + + +@pytest.mark.parametrize( + "depth_cm,absorption_coefficient_per_cm,result", + [(0.5, 2, 1000), (1.89, 0.316, 597.24), (0.0, 2.5, 0)], +) +def test_attenuation_at_depth_cm(depth_cm, absorption_coefficient_per_cm, result): + assert material_absorption_maths.attenuation_at_depth_cm( + depth_cm, absorption_coefficient_per_cm + ) == pytest.approx(result) + + +@pytest.mark.parametrize( + "target_attenuation_bn,absorption_coefficient_per_cm,result", + [ + (0, 2.4, 0), + (248.461, 2.13, 0.1166483568), + ], +) +def test_thickness_cm_required_to_attenuate( + target_attenuation_bn, absorption_coefficient_per_cm, result +): + assert material_absorption_maths.thickness_cm_required_to_attenuate( + target_attenuation_bn, absorption_coefficient_per_cm + ) == pytest.approx(result) + + +# inauspicious path +def test_attenuation_at_depth_cm_too_small_coefficient(): + with pytest.raises(ValueError): + material_absorption_maths.thickness_cm_required_to_attenuate(1, 1e-15) + + +def test_attenuation_at_depth_cm_too_negative_target(): + with pytest.raises(ValueError): + material_absorption_maths.thickness_cm_required_to_attenuate(-1, 1) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_thickness_cm_required_to_attenuate_target_error(bad_input): + with pytest.raises(TypeError): + material_absorption_maths.thickness_cm_required_to_attenuate(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_thickness_cm_required_to_attenuate_absorption_error(bad_input): + with pytest.raises(TypeError): + material_absorption_maths.thickness_cm_required_to_attenuate(1.0, bad_input) + + +@pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) +def test_attenuation_at_depth_cm_absorption_coefficient_error(bad_input): + with pytest.raises(ValueError): + material_absorption_maths.attenuation_at_depth_cm(1.0, bad_input) + + +@pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) +def test_attenuation_at_depth_cm_depth_negative_error(bad_input): + with pytest.raises(ValueError): + material_absorption_maths.attenuation_at_depth_cm(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_at_depth_cm_depth_depth_error(bad_input): + with pytest.raises(TypeError): + material_absorption_maths.attenuation_at_depth_cm(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_at_depth_cm_depth_absorption_error(bad_input): + with pytest.raises(TypeError): + material_absorption_maths.attenuation_at_depth_cm(1.0, bad_input) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_energy(bad_input): + with pytest.raises(TypeError): + material_absorption_maths.photon_mass_attenuation_per_unit_length( + bad_input, 1.0, 1.0 + ) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_absorp(bad_input): + with pytest.raises(TypeError): + material_absorption_maths.photon_mass_attenuation_per_unit_length( + 1.0, bad_input, 1.0 + ) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_expo(bad_input): + with pytest.raises(TypeError): + material_absorption_maths.photon_mass_attenuation_per_unit_length( + 1.0, 1.0, bad_input + ) From 7e8a3f5092ec62edc771e7f17a5518bbf8140426 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Tue, 5 May 2026 16:12:55 +0100 Subject: [PATCH 04/28] adds pydantic validation --- .../general_maths/material_absorption_maths.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py index 12bd3df206f..fd28f10ae17 100644 --- a/src/dodal/common/general_maths/material_absorption_maths.py +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -1,3 +1,5 @@ +from pydantic import PositiveFloat + from dodal.common.general_maths import transmission_interconversion @@ -22,7 +24,9 @@ def photon_mass_attenuation_per_unit_length( ) -def attenuation_at_depth_cm(depth_cm: float, absorption_coefficient_per_cm: float): +def attenuation_at_depth_cm( + depth_cm: PositiveFloat, absorption_coefficient_per_cm: PositiveFloat +): # TODO: check this - not sure if it really is the depth of the wedge? """Calculates attenuation in Barnett units. @@ -31,8 +35,8 @@ def attenuation_at_depth_cm(depth_cm: float, absorption_coefficient_per_cm: floa absorption_coefficient_per_cm (float): absorption coefficient per cm Raises: - ValueError: If either input value for either depth_cm or absorption_coefficient_ - per_cm <0.0, raises a value error. + ValueError: If either depth_cm or absorption_coefficient are negative, an error + is raised Returns: (float): attenuation in Barnett units @@ -44,13 +48,14 @@ def attenuation_at_depth_cm(depth_cm: float, absorption_coefficient_per_cm: floa "Invalid absorption, this calculator is no for systems with\ optical gain." ) + ln_t = -(depth_cm * absorption_coefficient_per_cm) return transmission_interconversion.attenuation_from_natural_log_of_transmission( - -(depth_cm * absorption_coefficient_per_cm) + ln_t ) def thickness_cm_required_to_attenuate( - target_attenuation_bn: float, + target_attenuation_bn: PositiveFloat, absorption_coefficient_per_cm: float, ): """Calculates material depth in cm. From 64c844c9c5da7408a09bfded264e2e0120d40687 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Tue, 5 May 2026 16:29:34 +0100 Subject: [PATCH 05/28] Added pydantic validation --- .../general_maths/material_absorption_maths.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py index fd28f10ae17..5e325e5b2c1 100644 --- a/src/dodal/common/general_maths/material_absorption_maths.py +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -1,4 +1,4 @@ -from pydantic import PositiveFloat +from pydantic import PositiveFloat, validate_call from dodal.common.general_maths import transmission_interconversion @@ -24,6 +24,7 @@ def photon_mass_attenuation_per_unit_length( ) +@validate_call() def attenuation_at_depth_cm( depth_cm: PositiveFloat, absorption_coefficient_per_cm: PositiveFloat ): @@ -41,19 +42,13 @@ def attenuation_at_depth_cm( Returns: (float): attenuation in Barnett units """ - if depth_cm < 0.0: - raise ValueError(f"Negative depth is an invalid input: {depth_cm}") - if absorption_coefficient_per_cm < 0.0: - raise ValueError( - "Invalid absorption, this calculator is no for systems with\ - optical gain." - ) ln_t = -(depth_cm * absorption_coefficient_per_cm) return transmission_interconversion.attenuation_from_natural_log_of_transmission( ln_t ) +@validate_call() def thickness_cm_required_to_attenuate( target_attenuation_bn: PositiveFloat, absorption_coefficient_per_cm: float, @@ -74,10 +69,7 @@ def thickness_cm_required_to_attenuate( (float): material depth in cm. """ minimum_meaningful_absorption_coefficient = 1.0e-14 - if ( - target_attenuation_bn < 0.0 - or absorption_coefficient_per_cm < minimum_meaningful_absorption_coefficient - ): + 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." From 4d48090855a150e33e9e28693cc7e1ffb19e62ad Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Tue, 5 May 2026 16:49:11 +0100 Subject: [PATCH 06/28] changes to NonNegativeFloat (accepting 0) where relevant --- .../general_maths/material_absorption_maths.py | 6 +++--- .../test_material_absorption_maths.py | 14 +++++++------- .../test_transmission_interconversion.py | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py index 5e325e5b2c1..e50c08acdb9 100644 --- a/src/dodal/common/general_maths/material_absorption_maths.py +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -1,4 +1,4 @@ -from pydantic import PositiveFloat, validate_call +from pydantic import NonNegativeFloat, PositiveFloat, validate_call from dodal.common.general_maths import transmission_interconversion @@ -26,7 +26,7 @@ def photon_mass_attenuation_per_unit_length( @validate_call() def attenuation_at_depth_cm( - depth_cm: PositiveFloat, absorption_coefficient_per_cm: PositiveFloat + depth_cm: NonNegativeFloat, absorption_coefficient_per_cm: PositiveFloat ): # TODO: check this - not sure if it really is the depth of the wedge? """Calculates attenuation in Barnett units. @@ -50,7 +50,7 @@ def attenuation_at_depth_cm( @validate_call() def thickness_cm_required_to_attenuate( - target_attenuation_bn: PositiveFloat, + target_attenuation_bn: NonNegativeFloat, absorption_coefficient_per_cm: float, ): """Calculates material depth in cm. diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py index 9d5b5695813..b20c8465e49 100644 --- a/tests/common/general_maths/test_material_absorption_maths.py +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -24,7 +24,7 @@ def test_photon_mass_attenuation_per_unit_length( ): assert material_absorption_maths.photon_mass_attenuation_per_unit_length( energy_kev, photon_absorption_factor_per_unit_length, energy_dependence_exponent - ) == pytest.approx(result) + ) == pytest.approx(result, rel=1e-6) @pytest.mark.parametrize( @@ -34,7 +34,7 @@ def test_photon_mass_attenuation_per_unit_length( def test_attenuation_at_depth_cm(depth_cm, absorption_coefficient_per_cm, result): assert material_absorption_maths.attenuation_at_depth_cm( depth_cm, absorption_coefficient_per_cm - ) == pytest.approx(result) + ) == pytest.approx(result, rel=1e-6) @pytest.mark.parametrize( @@ -49,7 +49,7 @@ def test_thickness_cm_required_to_attenuate( ): assert material_absorption_maths.thickness_cm_required_to_attenuate( target_attenuation_bn, absorption_coefficient_per_cm - ) == pytest.approx(result) + ) == pytest.approx(result, rel=1e-6) # inauspicious path @@ -65,13 +65,13 @@ def test_attenuation_at_depth_cm_too_negative_target(): @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) def test_thickness_cm_required_to_attenuate_target_error(bad_input): - with pytest.raises(TypeError): + with pytest.raises(ValueError): material_absorption_maths.thickness_cm_required_to_attenuate(bad_input, 1.0) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) def test_thickness_cm_required_to_attenuate_absorption_error(bad_input): - with pytest.raises(TypeError): + with pytest.raises(ValueError): material_absorption_maths.thickness_cm_required_to_attenuate(1.0, bad_input) @@ -89,13 +89,13 @@ def test_attenuation_at_depth_cm_depth_negative_error(bad_input): @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) def test_attenuation_at_depth_cm_depth_depth_error(bad_input): - with pytest.raises(TypeError): + with pytest.raises(ValueError): material_absorption_maths.attenuation_at_depth_cm(bad_input, 1.0) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) def test_attenuation_at_depth_cm_depth_absorption_error(bad_input): - with pytest.raises(TypeError): + with pytest.raises(ValueError): material_absorption_maths.attenuation_at_depth_cm(1.0, bad_input) diff --git a/tests/common/general_maths/test_transmission_interconversion.py b/tests/common/general_maths/test_transmission_interconversion.py index cb1a376a167..711b1322e26 100644 --- a/tests/common/general_maths/test_transmission_interconversion.py +++ b/tests/common/general_maths/test_transmission_interconversion.py @@ -12,7 +12,7 @@ def test_attenuation_from_natural_log_of_transmission(ln_t, result): assert transmission_interconversion.attenuation_from_natural_log_of_transmission( ln_t - ) == pytest.approx(result) + ) == pytest.approx(result, rel=1e-6) @pytest.mark.parametrize( @@ -22,7 +22,7 @@ def test_attenuation_from_natural_log_of_transmission(ln_t, result): def test_attenuation_from_transmission(transmission_as_fraction, result): assert transmission_interconversion.attenuation_from_transmission( transmission_as_fraction - ) == pytest.approx(result) + ) == pytest.approx(result, rel=1e-6) @pytest.mark.parametrize( @@ -32,7 +32,7 @@ def test_attenuation_from_transmission(transmission_as_fraction, result): def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): assert transmission_interconversion.natural_log_of_transmission_from_attenuation( attenuation_bn - ) == pytest.approx(result) + ) == pytest.approx(result, rel=1e-6) @pytest.mark.parametrize( @@ -42,7 +42,7 @@ def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): def test_transmission_from_attenutation(attenuation_bn, result): assert transmission_interconversion.transmission_from_attenutation( attenuation_bn - ) == pytest.approx(result) + ) == pytest.approx(result, rel=1e-6) # inauspicious: From da5185507701de2761aa94787484cb83f1a2bb59 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Tue, 5 May 2026 16:56:15 +0100 Subject: [PATCH 07/28] changes to NonNegativeFloat (accepting 0) where relevant, considering how best to split functions - will be done next time --- src/dodal/common/general_maths/material_absorption_maths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py index e50c08acdb9..ecc219f7e17 100644 --- a/src/dodal/common/general_maths/material_absorption_maths.py +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -26,7 +26,7 @@ def photon_mass_attenuation_per_unit_length( @validate_call() def attenuation_at_depth_cm( - depth_cm: NonNegativeFloat, absorption_coefficient_per_cm: PositiveFloat + depth_cm: NonNegativeFloat, absorption_coefficient_per_cm: NonNegativeFloat ): # TODO: check this - not sure if it really is the depth of the wedge? """Calculates attenuation in Barnett units. From e5c341dca2917b8b44f6b40a4dca635f0c0463d5 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Tue, 5 May 2026 16:59:56 +0100 Subject: [PATCH 08/28] fixes linting. going to move tests next update. --- src/dodal/common/general_maths/material_absorption_maths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py index ecc219f7e17..23eaf723ab2 100644 --- a/src/dodal/common/general_maths/material_absorption_maths.py +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -1,4 +1,4 @@ -from pydantic import NonNegativeFloat, PositiveFloat, validate_call +from pydantic import NonNegativeFloat, validate_call from dodal.common.general_maths import transmission_interconversion From 5215f961d73fbd9cb3bc98f6e8bfd386b6b8db6a Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 6 May 2026 09:37:42 +0100 Subject: [PATCH 09/28] labels tests --- .../transmission_interconversion.py | 9 ++- .../test_transmission_interconversion.py | 70 ++++++++++++++++--- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/dodal/common/general_maths/transmission_interconversion.py b/src/dodal/common/general_maths/transmission_interconversion.py index e2efaba9cb0..7ae8d2e0f41 100644 --- a/src/dodal/common/general_maths/transmission_interconversion.py +++ b/src/dodal/common/general_maths/transmission_interconversion.py @@ -1,8 +1,5 @@ import math -canonical_barnett_conversion = -1.0e3 -reverse_barnett_conversion = -1.0e-3 - def attenuation_from_natural_log_of_transmission(ln_t: float): """Converts from natural log of transmission fraction into Barnett attenuation units @@ -14,7 +11,8 @@ def attenuation_from_natural_log_of_transmission(ln_t: float): Returns: (float): Barnett attenuation units """ - return canonical_barnett_conversion * ln_t + _canonical_barnett_conversion = -1.0e3 + return _canonical_barnett_conversion * ln_t def attenuation_from_transmission(transmission_as_fraction: float): @@ -40,7 +38,8 @@ def natural_log_of_transmission_from_attenuation(attenuation_bn: float): Returns: (float): natural log of transmission fraction """ - return reverse_barnett_conversion * attenuation_bn + _reverse_barnett_conversion = -1.0e-3 + return _reverse_barnett_conversion * attenuation_bn def transmission_from_attenutation(attenuation_bn: float): diff --git a/tests/common/general_maths/test_transmission_interconversion.py b/tests/common/general_maths/test_transmission_interconversion.py index cb1a376a167..158fc106ef5 100644 --- a/tests/common/general_maths/test_transmission_interconversion.py +++ b/tests/common/general_maths/test_transmission_interconversion.py @@ -7,7 +7,19 @@ # happy: @pytest.mark.parametrize( - "ln_t,result", [(-1, 1000), (0, 0), (-0.4367, 436.7), (-5.9017, 5901.7)] + "ln_t,result", + [ + (-1, 1000), # tests negative unity log of transmission is 1000 (canonical) + (0, 0), # tests natural log of transparency is zero (canonical) + ( + -0.4367, + 436.7, + ), # tests log from arbitrary high transmission is -1000 * log + ( + -5.9017, + 5901.7, + ), # tests log from arbitrary low transmission is -1000 * log + ], ) def test_attenuation_from_natural_log_of_transmission(ln_t, result): assert transmission_interconversion.attenuation_from_natural_log_of_transmission( @@ -17,7 +29,18 @@ def test_attenuation_from_natural_log_of_transmission(ln_t, result): @pytest.mark.parametrize( "transmission_as_fraction,result", - [(1, 0), (0.37, 994.25227334), (0.871, 138.1133), (4.2e-4, 7775.2558)], + [ + (1, 0), # tests attenuation from transparency is zero (canonical) + (0.37, 994.25227334), # tests attenuation from 37% is close to 1000 (canonical) + ( + 0.871, + 138.1133, + ), # tests attenuation from arbitrary high transmission has expected value + ( + 4.2e-4, + 7775.2558, + ), # tests attenuation from arbitrary high attenuation has expected value + ], ) def test_attenuation_from_transmission(transmission_as_fraction, result): assert transmission_interconversion.attenuation_from_transmission( @@ -27,7 +50,24 @@ def test_attenuation_from_transmission(transmission_as_fraction, result): @pytest.mark.parametrize( "attenuation_bn,result", - [(0, 0), (1e3, -1), (712.6, -0.7126), (8034.1, -8.0341)], + [ + (0, 0), # tests natural log of transparency from attenuation (canonical) + ( + 1e3, + -1, + ), # tests natural log of transmission from canonical transmission is negative + # unity (canonical) + ( + 712.6, + -0.7126, + ), # tests attenuation from natural log arbitrary high transmission is -1000 * + # attenuation + ( + 8034.1, + -8.0341, + ), # tests attenuation from natural log arbitrary low transmission is -1000 * + # attenuation + ], ) def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): assert transmission_interconversion.natural_log_of_transmission_from_attenuation( @@ -37,7 +77,21 @@ def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): @pytest.mark.parametrize( "attenuation_bn,result", - [(0, 1), (1e3, 0.3678794), (145.1, 0.8649358), (7221.9, 7.3041331435179e-4)], + [ + (0, 1), # tests transparency from zero attenuation (canonical) + ( + 1e3, + 0.3678794, + ), # tests transmission from canonical attenuation is 1/e (canonical) + ( + 145.1, + 0.8649358, + ), # tests transmission from arbitrary weak attenuation is -1000*attenuation + ( + 7221.9, + 7.3041331435179e-4, + ), # tests transmission from arbitrary strong attenuation is -1000*attenuation + ], ) def test_transmission_from_attenutation(attenuation_bn, result): assert transmission_interconversion.transmission_from_attenutation( @@ -49,7 +103,7 @@ def test_transmission_from_attenutation(attenuation_bn, result): @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_from_natural_log_of_transmission_errors(bad_input): +def test_attenuation_from_natural_log_of_transmission_raises_error(bad_input): with pytest.raises(TypeError): transmission_interconversion.attenuation_from_natural_log_of_transmission( bad_input @@ -57,13 +111,13 @@ def test_attenuation_from_natural_log_of_transmission_errors(bad_input): @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_from_transmission_errors(bad_input): +def test_attenuation_from_transmission_raises_error(bad_input): with pytest.raises(TypeError): transmission_interconversion.attenuation_from_transmission(bad_input) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_natural_log_of_transmission_from_attenuation_errors(bad_input): +def test_natural_log_of_transmission_from_attenuation_raises_error(bad_input): with pytest.raises(TypeError): transmission_interconversion.natural_log_of_transmission_from_attenuation( bad_input @@ -71,6 +125,6 @@ def test_natural_log_of_transmission_from_attenuation_errors(bad_input): @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_transmission_from_attenutation_errors(bad_input): +def test_transmission_from_attenutation_raises_error(bad_input): with pytest.raises(TypeError): transmission_interconversion.transmission_from_attenutation(bad_input) From 273cbff9ec414474d9880f943d16d41247eadc25 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 6 May 2026 09:52:17 +0100 Subject: [PATCH 10/28] added test labels --- .../test_material_absorption_maths.py | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py index b20c8465e49..72407e081ac 100644 --- a/tests/common/general_maths/test_material_absorption_maths.py +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -10,10 +10,10 @@ "energy_kev,photon_absorption_factor_per_unit_length,energy_dependence_exponent," "result", [ - (5.042, 1.98e2, -2.717, 2.44170544), # Arbitrary - (8.3328, 2.5706e3, -2.83, 6.3708311), # Nickel - (11.9187, 1.48e3, -2.93, 1.03970725), # Gold-Three - (25.514, 6.48e3, -2.41, 2.63778077), # Silver + (5.042, 1.98e2, -2.717, 2.44170544), # Arbitrary energy + (8.3328, 2.5706e3, -2.83, 6.3708311), # Nickel energy + (11.9187, 1.48e3, -2.93, 1.03970725), # Gold-Three energy + (25.514, 6.48e3, -2.41, 2.63778077), # Silver energy ], ) def test_photon_mass_attenuation_per_unit_length( @@ -29,7 +29,19 @@ def test_photon_mass_attenuation_per_unit_length( @pytest.mark.parametrize( "depth_cm,absorption_coefficient_per_cm,result", - [(0.5, 2, 1000), (1.89, 0.316, 597.24), (0.0, 2.5, 0)], + [ + ( + 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 material_absorption_maths.attenuation_at_depth_cm( @@ -40,8 +52,12 @@ def test_attenuation_at_depth_cm(depth_cm, absorption_coefficient_per_cm, result @pytest.mark.parametrize( "target_attenuation_bn,absorption_coefficient_per_cm,result", [ - (0, 2.4, 0), - (248.461, 2.13, 0.1166483568), + (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( From c9ef8249d091308abaca10d12fa8fb92a33febf9 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 6 May 2026 12:14:32 +0100 Subject: [PATCH 11/28] split tests --- .../test_attenuation_from_ln_transmission.py | 38 +++++ .../test_attenuation_from_transmission.py | 37 +++++ .../test_ln_transmission_from_attenuation.py | 42 ++++++ .../test_transmission_from_attenuation.py | 39 ++++++ .../test_transmission_interconversion.py | 130 ------------------ 5 files changed, 156 insertions(+), 130 deletions(-) create mode 100644 tests/common/general_maths/test_attenuation_from_ln_transmission.py create mode 100644 tests/common/general_maths/test_attenuation_from_transmission.py create mode 100644 tests/common/general_maths/test_ln_transmission_from_attenuation.py create mode 100644 tests/common/general_maths/test_transmission_from_attenuation.py delete mode 100644 tests/common/general_maths/test_transmission_interconversion.py diff --git a/tests/common/general_maths/test_attenuation_from_ln_transmission.py b/tests/common/general_maths/test_attenuation_from_ln_transmission.py new file mode 100644 index 00000000000..93bb2ab07a6 --- /dev/null +++ b/tests/common/general_maths/test_attenuation_from_ln_transmission.py @@ -0,0 +1,38 @@ +import math + +import pytest + +from dodal.common.general_maths import transmission_interconversion + + +# happy path +@pytest.mark.parametrize( + "ln_t,result", + [ + (-1, 1000), # tests negative unity log of transmission is 1000 (canonical) + (0, 0), # tests natural log of transparency is zero (canonical) + ( + -0.4367, + 436.7, + ), # tests log from arbitrary high transmission is -1000 * log + ( + -5.9017, + 5901.7, + ), # tests log from arbitrary low transmission is -1000 * log + ], +) +def test_attenuation_from_natural_log_of_transmission(ln_t, result): + assert transmission_interconversion.attenuation_from_natural_log_of_transmission( + ln_t + ) == pytest.approx(result) + + +# inauspicious: + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_from_natural_log_of_transmission_raises_error(bad_input): + with pytest.raises(TypeError): + transmission_interconversion.attenuation_from_natural_log_of_transmission( + bad_input + ) diff --git a/tests/common/general_maths/test_attenuation_from_transmission.py b/tests/common/general_maths/test_attenuation_from_transmission.py new file mode 100644 index 00000000000..56f868f09ea --- /dev/null +++ b/tests/common/general_maths/test_attenuation_from_transmission.py @@ -0,0 +1,37 @@ +import math + +import pytest + +from dodal.common.general_maths import transmission_interconversion + +# happy: + + +@pytest.mark.parametrize( + "transmission_as_fraction,result", + [ + (1, 0), # tests attenuation from transparency is zero (canonical) + (0.37, 994.25227334), # tests attenuation from 37% is close to 1000 (canonical) + ( + 0.871, + 138.1133, + ), # tests attenuation from arbitrary high transmission has expected value + ( + 4.2e-4, + 7775.2558, + ), # tests attenuation from arbitrary high attenuation has expected value + ], +) +def test_attenuation_from_transmission(transmission_as_fraction, result): + assert transmission_interconversion.attenuation_from_transmission( + transmission_as_fraction + ) == pytest.approx(result) + + +# inauspicious: + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_from_transmission_raises_error(bad_input): + with pytest.raises(TypeError): + transmission_interconversion.attenuation_from_transmission(bad_input) diff --git a/tests/common/general_maths/test_ln_transmission_from_attenuation.py b/tests/common/general_maths/test_ln_transmission_from_attenuation.py new file mode 100644 index 00000000000..5ed11143f49 --- /dev/null +++ b/tests/common/general_maths/test_ln_transmission_from_attenuation.py @@ -0,0 +1,42 @@ +import math + +import pytest + +from dodal.common.general_maths import transmission_interconversion + + +# happy: +@pytest.mark.parametrize( + "attenuation_bn,result", + [ + (0, 0), # tests natural log of transparency from attenuation (canonical) + ( + 1e3, + -1, + ), # tests natural log of transmission from canonical transmission is negative + # unity (canonical) + ( + 712.6, + -0.7126, + ), # tests attenuation from natural log arbitrary high transmission is -1000 * + # attenuation + ( + 8034.1, + -8.0341, + ), # tests attenuation from natural log arbitrary low transmission is -1000 * + # attenuation + ], +) +def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): + assert transmission_interconversion.natural_log_of_transmission_from_attenuation( + attenuation_bn + ) == pytest.approx(result) + + +# inauspicious: +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_natural_log_of_transmission_from_attenuation_raises_error(bad_input): + with pytest.raises(TypeError): + transmission_interconversion.natural_log_of_transmission_from_attenuation( + bad_input + ) diff --git a/tests/common/general_maths/test_transmission_from_attenuation.py b/tests/common/general_maths/test_transmission_from_attenuation.py new file mode 100644 index 00000000000..75370b9c84a --- /dev/null +++ b/tests/common/general_maths/test_transmission_from_attenuation.py @@ -0,0 +1,39 @@ +import math + +import pytest + +from dodal.common.general_maths import transmission_interconversion + + +# happy: +@pytest.mark.parametrize( + "attenuation_bn,result", + [ + (0, 1), # tests transparency from zero attenuation (canonical) + ( + 1e3, + 0.3678794, + ), # tests transmission from canonical attenuation is 1/e (canonical) + ( + 145.1, + 0.8649358, + ), # tests transmission from arbitrary weak attenuation is -1000*attenuation + ( + 7221.9, + 7.3041331435179e-4, + ), # tests transmission from arbitrary strong attenuation is -1000*attenuation + ], +) +def test_transmission_from_attenutation(attenuation_bn, result): + assert transmission_interconversion.transmission_from_attenutation( + attenuation_bn + ) == pytest.approx(result) + + +# inauspicious: + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_transmission_from_attenutation_raises_error(bad_input): + with pytest.raises(TypeError): + transmission_interconversion.transmission_from_attenutation(bad_input) diff --git a/tests/common/general_maths/test_transmission_interconversion.py b/tests/common/general_maths/test_transmission_interconversion.py deleted file mode 100644 index 158fc106ef5..00000000000 --- a/tests/common/general_maths/test_transmission_interconversion.py +++ /dev/null @@ -1,130 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths import transmission_interconversion - - -# happy: -@pytest.mark.parametrize( - "ln_t,result", - [ - (-1, 1000), # tests negative unity log of transmission is 1000 (canonical) - (0, 0), # tests natural log of transparency is zero (canonical) - ( - -0.4367, - 436.7, - ), # tests log from arbitrary high transmission is -1000 * log - ( - -5.9017, - 5901.7, - ), # tests log from arbitrary low transmission is -1000 * log - ], -) -def test_attenuation_from_natural_log_of_transmission(ln_t, result): - assert transmission_interconversion.attenuation_from_natural_log_of_transmission( - ln_t - ) == pytest.approx(result) - - -@pytest.mark.parametrize( - "transmission_as_fraction,result", - [ - (1, 0), # tests attenuation from transparency is zero (canonical) - (0.37, 994.25227334), # tests attenuation from 37% is close to 1000 (canonical) - ( - 0.871, - 138.1133, - ), # tests attenuation from arbitrary high transmission has expected value - ( - 4.2e-4, - 7775.2558, - ), # tests attenuation from arbitrary high attenuation has expected value - ], -) -def test_attenuation_from_transmission(transmission_as_fraction, result): - assert transmission_interconversion.attenuation_from_transmission( - transmission_as_fraction - ) == pytest.approx(result) - - -@pytest.mark.parametrize( - "attenuation_bn,result", - [ - (0, 0), # tests natural log of transparency from attenuation (canonical) - ( - 1e3, - -1, - ), # tests natural log of transmission from canonical transmission is negative - # unity (canonical) - ( - 712.6, - -0.7126, - ), # tests attenuation from natural log arbitrary high transmission is -1000 * - # attenuation - ( - 8034.1, - -8.0341, - ), # tests attenuation from natural log arbitrary low transmission is -1000 * - # attenuation - ], -) -def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): - assert transmission_interconversion.natural_log_of_transmission_from_attenuation( - attenuation_bn - ) == pytest.approx(result) - - -@pytest.mark.parametrize( - "attenuation_bn,result", - [ - (0, 1), # tests transparency from zero attenuation (canonical) - ( - 1e3, - 0.3678794, - ), # tests transmission from canonical attenuation is 1/e (canonical) - ( - 145.1, - 0.8649358, - ), # tests transmission from arbitrary weak attenuation is -1000*attenuation - ( - 7221.9, - 7.3041331435179e-4, - ), # tests transmission from arbitrary strong attenuation is -1000*attenuation - ], -) -def test_transmission_from_attenutation(attenuation_bn, result): - assert transmission_interconversion.transmission_from_attenutation( - attenuation_bn - ) == pytest.approx(result) - - -# inauspicious: - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_from_natural_log_of_transmission_raises_error(bad_input): - with pytest.raises(TypeError): - transmission_interconversion.attenuation_from_natural_log_of_transmission( - bad_input - ) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_from_transmission_raises_error(bad_input): - with pytest.raises(TypeError): - transmission_interconversion.attenuation_from_transmission(bad_input) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_natural_log_of_transmission_from_attenuation_raises_error(bad_input): - with pytest.raises(TypeError): - transmission_interconversion.natural_log_of_transmission_from_attenuation( - bad_input - ) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_transmission_from_attenutation_raises_error(bad_input): - with pytest.raises(TypeError): - transmission_interconversion.transmission_from_attenutation(bad_input) From 7d94b52fa5bc0281730893b1071a8c52210c3f4a Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 6 May 2026 12:24:27 +0100 Subject: [PATCH 12/28] split tests --- .../test_attenuation_at_depth.py | 55 +++++++ .../test_material_absorption_maths.py | 139 ------------------ .../test_photon_mass_attenuation.py | 52 +++++++ .../test_thickness_required_to_attenuate.py | 48 ++++++ 4 files changed, 155 insertions(+), 139 deletions(-) create mode 100644 tests/common/general_maths/test_attenuation_at_depth.py delete mode 100644 tests/common/general_maths/test_material_absorption_maths.py create mode 100644 tests/common/general_maths/test_photon_mass_attenuation.py create mode 100644 tests/common/general_maths/test_thickness_required_to_attenuate.py diff --git a/tests/common/general_maths/test_attenuation_at_depth.py b/tests/common/general_maths/test_attenuation_at_depth.py new file mode 100644 index 00000000000..bb5d1428deb --- /dev/null +++ b/tests/common/general_maths/test_attenuation_at_depth.py @@ -0,0 +1,55 @@ +import math + +import pytest + +from dodal.common.general_maths import material_absorption_maths + + +# happy path +@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 material_absorption_maths.attenuation_at_depth_cm( + depth_cm, absorption_coefficient_per_cm + ) == pytest.approx(result, rel=1e-6) + + +# inauspicious path + + +@pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) +def test_attenuation_at_depth_cm_absorption_coefficient_error(bad_input): + with pytest.raises(ValueError): + material_absorption_maths.attenuation_at_depth_cm(1.0, bad_input) + + +@pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) +def test_attenuation_at_depth_cm_depth_raises_negative_error(bad_input): + with pytest.raises(ValueError): + material_absorption_maths.attenuation_at_depth_cm(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_at_depth_cm_depth_depth_raises_error(bad_input): + with pytest.raises(ValueError): + material_absorption_maths.attenuation_at_depth_cm(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_at_depth_cm_depth_absorption_raises_error(bad_input): + with pytest.raises(ValueError): + material_absorption_maths.attenuation_at_depth_cm(1.0, bad_input) diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py deleted file mode 100644 index 72407e081ac..00000000000 --- a/tests/common/general_maths/test_material_absorption_maths.py +++ /dev/null @@ -1,139 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths import material_absorption_maths - - -# happy path -@pytest.mark.parametrize( - "energy_kev,photon_absorption_factor_per_unit_length,energy_dependence_exponent," - "result", - [ - (5.042, 1.98e2, -2.717, 2.44170544), # Arbitrary energy - (8.3328, 2.5706e3, -2.83, 6.3708311), # Nickel energy - (11.9187, 1.48e3, -2.93, 1.03970725), # Gold-Three energy - (25.514, 6.48e3, -2.41, 2.63778077), # Silver energy - ], -) -def test_photon_mass_attenuation_per_unit_length( - energy_kev, - photon_absorption_factor_per_unit_length, - energy_dependence_exponent, - result, -): - assert material_absorption_maths.photon_mass_attenuation_per_unit_length( - energy_kev, photon_absorption_factor_per_unit_length, energy_dependence_exponent - ) == 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 material_absorption_maths.attenuation_at_depth_cm( - depth_cm, absorption_coefficient_per_cm - ) == pytest.approx(result, rel=1e-6) - - -@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 material_absorption_maths.thickness_cm_required_to_attenuate( - target_attenuation_bn, absorption_coefficient_per_cm - ) == pytest.approx(result, rel=1e-6) - - -# inauspicious path -def test_attenuation_at_depth_cm_too_small_coefficient(): - with pytest.raises(ValueError): - material_absorption_maths.thickness_cm_required_to_attenuate(1, 1e-15) - - -def test_attenuation_at_depth_cm_too_negative_target(): - with pytest.raises(ValueError): - material_absorption_maths.thickness_cm_required_to_attenuate(-1, 1) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_thickness_cm_required_to_attenuate_target_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.thickness_cm_required_to_attenuate(bad_input, 1.0) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_thickness_cm_required_to_attenuate_absorption_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.thickness_cm_required_to_attenuate(1.0, bad_input) - - -@pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) -def test_attenuation_at_depth_cm_absorption_coefficient_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.attenuation_at_depth_cm(1.0, bad_input) - - -@pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) -def test_attenuation_at_depth_cm_depth_negative_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.attenuation_at_depth_cm(bad_input, 1.0) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_at_depth_cm_depth_depth_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.attenuation_at_depth_cm(bad_input, 1.0) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_at_depth_cm_depth_absorption_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.attenuation_at_depth_cm(1.0, bad_input) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_photon_mass_attenuation_per_unit_length_errors_energy(bad_input): - with pytest.raises(TypeError): - material_absorption_maths.photon_mass_attenuation_per_unit_length( - bad_input, 1.0, 1.0 - ) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_photon_mass_attenuation_per_unit_length_errors_absorp(bad_input): - with pytest.raises(TypeError): - material_absorption_maths.photon_mass_attenuation_per_unit_length( - 1.0, bad_input, 1.0 - ) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_photon_mass_attenuation_per_unit_length_errors_expo(bad_input): - with pytest.raises(TypeError): - material_absorption_maths.photon_mass_attenuation_per_unit_length( - 1.0, 1.0, bad_input - ) diff --git a/tests/common/general_maths/test_photon_mass_attenuation.py b/tests/common/general_maths/test_photon_mass_attenuation.py new file mode 100644 index 00000000000..a5647372ede --- /dev/null +++ b/tests/common/general_maths/test_photon_mass_attenuation.py @@ -0,0 +1,52 @@ +import math + +import pytest + +from dodal.common.general_maths import material_absorption_maths + + +# happy path +@pytest.mark.parametrize( + "energy_kev,photon_absorption_factor_per_unit_length,energy_dependence_exponent," + "result", + [ + (5.042, 1.98e2, -2.717, 2.44170544), # Arbitrary + (8.3328, 2.5706e3, -2.83, 6.3708311), # Nickel + (11.9187, 1.48e3, -2.93, 1.03970725), # Gold-Three + (25.514, 6.48e3, -2.41, 2.63778077), # Silver + ], +) +def test_photon_mass_attenuation_per_unit_length( + energy_kev, + photon_absorption_factor_per_unit_length, + energy_dependence_exponent, + result, +): + assert material_absorption_maths.photon_mass_attenuation_per_unit_length( + energy_kev, photon_absorption_factor_per_unit_length, energy_dependence_exponent + ) == pytest.approx(result) + + +# inauspicious path +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_energy(bad_input): + with pytest.raises(TypeError): + material_absorption_maths.photon_mass_attenuation_per_unit_length( + bad_input, 1.0, 1.0 + ) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_absorp(bad_input): + with pytest.raises(TypeError): + material_absorption_maths.photon_mass_attenuation_per_unit_length( + 1.0, bad_input, 1.0 + ) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_expo(bad_input): + with pytest.raises(TypeError): + material_absorption_maths.photon_mass_attenuation_per_unit_length( + 1.0, 1.0, bad_input + ) diff --git a/tests/common/general_maths/test_thickness_required_to_attenuate.py b/tests/common/general_maths/test_thickness_required_to_attenuate.py new file mode 100644 index 00000000000..4c47a1f42ec --- /dev/null +++ b/tests/common/general_maths/test_thickness_required_to_attenuate.py @@ -0,0 +1,48 @@ +import math + +import pytest + +from dodal.common.general_maths import material_absorption_maths + + +# happy path +@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 material_absorption_maths.thickness_cm_required_to_attenuate( + target_attenuation_bn, absorption_coefficient_per_cm + ) == pytest.approx(result, rel=1e-6) + + +# inauspicious path +def test_thickness_cm_required_to_attenuate_too_small_coefficient(): + with pytest.raises(ValueError): + material_absorption_maths.thickness_cm_required_to_attenuate(1, 1e-15) + + +def test_thickness_required_to_attenuate_raises_negative_error(): + with pytest.raises(ValueError): + material_absorption_maths.thickness_cm_required_to_attenuate(-1, 1) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_thickness_cm_required_to_attenuate_target_raises_error(bad_input): + with pytest.raises(ValueError): + material_absorption_maths.thickness_cm_required_to_attenuate(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_thickness_cm_required_to_attenuate_absorption_raises_error(bad_input): + with pytest.raises(ValueError): + material_absorption_maths.thickness_cm_required_to_attenuate(1.0, bad_input) From 10f65f8d5ae8d4003624e3ed8fee3e58d998cf0f Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 6 May 2026 15:24:58 +0100 Subject: [PATCH 13/28] adds specific output --- .../transmission_interconversion.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/dodal/common/general_maths/transmission_interconversion.py b/src/dodal/common/general_maths/transmission_interconversion.py index 7ae8d2e0f41..70876148a9d 100644 --- a/src/dodal/common/general_maths/transmission_interconversion.py +++ b/src/dodal/common/general_maths/transmission_interconversion.py @@ -1,7 +1,10 @@ import math +CANONICAL_BARNETT_CONVERSION = -1.0e3 +REVERSE_BARNETT_CONVERSION = -1.0e-3 -def attenuation_from_natural_log_of_transmission(ln_t: float): + +def attenuation_from_natural_log_of_transmission(ln_t: float) -> float: """Converts from natural log of transmission fraction into Barnett attenuation units . @@ -11,11 +14,10 @@ def attenuation_from_natural_log_of_transmission(ln_t: float): Returns: (float): Barnett attenuation units """ - _canonical_barnett_conversion = -1.0e3 - return _canonical_barnett_conversion * ln_t + return CANONICAL_BARNETT_CONVERSION * ln_t -def attenuation_from_transmission(transmission_as_fraction: float): +def attenuation_from_transmission(transmission_as_fraction: float) -> float: """Converts from transmission fraction into Barnett attenuation units. Args: @@ -28,7 +30,7 @@ def attenuation_from_transmission(transmission_as_fraction: float): return attenuation_from_natural_log_of_transmission(ln_t) -def natural_log_of_transmission_from_attenuation(attenuation_bn: float): +def natural_log_of_transmission_from_attenuation(attenuation_bn: float) -> float: """Converts from Barnett attenuation units into natural log of transmission fraction . @@ -38,11 +40,10 @@ def natural_log_of_transmission_from_attenuation(attenuation_bn: float): Returns: (float): natural log of transmission fraction """ - _reverse_barnett_conversion = -1.0e-3 - return _reverse_barnett_conversion * attenuation_bn + return REVERSE_BARNETT_CONVERSION * attenuation_bn -def transmission_from_attenutation(attenuation_bn: float): +def transmission_from_attenutation(attenuation_bn: float) -> float: """Converts from Barnett attenuation units into transmission fraction. Args: From c243228beecfbe1e28ae729806c8e368143918b6 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 11:39:16 +0100 Subject: [PATCH 14/28] rejoins tests --- .../test_attenuation_from_ln_transmission.py | 38 ------ .../test_attenuation_from_transmission.py | 37 ------ .../test_ln_transmission_from_attenuation.py | 42 ------ .../test_transmission_from_attenuation.py | 39 ------ .../test_transmission_inerconversion.py | 124 ++++++++++++++++++ 5 files changed, 124 insertions(+), 156 deletions(-) delete mode 100644 tests/common/general_maths/test_attenuation_from_ln_transmission.py delete mode 100644 tests/common/general_maths/test_attenuation_from_transmission.py delete mode 100644 tests/common/general_maths/test_ln_transmission_from_attenuation.py delete mode 100644 tests/common/general_maths/test_transmission_from_attenuation.py create mode 100644 tests/common/general_maths/test_transmission_inerconversion.py diff --git a/tests/common/general_maths/test_attenuation_from_ln_transmission.py b/tests/common/general_maths/test_attenuation_from_ln_transmission.py deleted file mode 100644 index 93bb2ab07a6..00000000000 --- a/tests/common/general_maths/test_attenuation_from_ln_transmission.py +++ /dev/null @@ -1,38 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths import transmission_interconversion - - -# happy path -@pytest.mark.parametrize( - "ln_t,result", - [ - (-1, 1000), # tests negative unity log of transmission is 1000 (canonical) - (0, 0), # tests natural log of transparency is zero (canonical) - ( - -0.4367, - 436.7, - ), # tests log from arbitrary high transmission is -1000 * log - ( - -5.9017, - 5901.7, - ), # tests log from arbitrary low transmission is -1000 * log - ], -) -def test_attenuation_from_natural_log_of_transmission(ln_t, result): - assert transmission_interconversion.attenuation_from_natural_log_of_transmission( - ln_t - ) == pytest.approx(result) - - -# inauspicious: - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_from_natural_log_of_transmission_raises_error(bad_input): - with pytest.raises(TypeError): - transmission_interconversion.attenuation_from_natural_log_of_transmission( - bad_input - ) diff --git a/tests/common/general_maths/test_attenuation_from_transmission.py b/tests/common/general_maths/test_attenuation_from_transmission.py deleted file mode 100644 index 56f868f09ea..00000000000 --- a/tests/common/general_maths/test_attenuation_from_transmission.py +++ /dev/null @@ -1,37 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths import transmission_interconversion - -# happy: - - -@pytest.mark.parametrize( - "transmission_as_fraction,result", - [ - (1, 0), # tests attenuation from transparency is zero (canonical) - (0.37, 994.25227334), # tests attenuation from 37% is close to 1000 (canonical) - ( - 0.871, - 138.1133, - ), # tests attenuation from arbitrary high transmission has expected value - ( - 4.2e-4, - 7775.2558, - ), # tests attenuation from arbitrary high attenuation has expected value - ], -) -def test_attenuation_from_transmission(transmission_as_fraction, result): - assert transmission_interconversion.attenuation_from_transmission( - transmission_as_fraction - ) == pytest.approx(result) - - -# inauspicious: - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_from_transmission_raises_error(bad_input): - with pytest.raises(TypeError): - transmission_interconversion.attenuation_from_transmission(bad_input) diff --git a/tests/common/general_maths/test_ln_transmission_from_attenuation.py b/tests/common/general_maths/test_ln_transmission_from_attenuation.py deleted file mode 100644 index 5ed11143f49..00000000000 --- a/tests/common/general_maths/test_ln_transmission_from_attenuation.py +++ /dev/null @@ -1,42 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths import transmission_interconversion - - -# happy: -@pytest.mark.parametrize( - "attenuation_bn,result", - [ - (0, 0), # tests natural log of transparency from attenuation (canonical) - ( - 1e3, - -1, - ), # tests natural log of transmission from canonical transmission is negative - # unity (canonical) - ( - 712.6, - -0.7126, - ), # tests attenuation from natural log arbitrary high transmission is -1000 * - # attenuation - ( - 8034.1, - -8.0341, - ), # tests attenuation from natural log arbitrary low transmission is -1000 * - # attenuation - ], -) -def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): - assert transmission_interconversion.natural_log_of_transmission_from_attenuation( - attenuation_bn - ) == pytest.approx(result) - - -# inauspicious: -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_natural_log_of_transmission_from_attenuation_raises_error(bad_input): - with pytest.raises(TypeError): - transmission_interconversion.natural_log_of_transmission_from_attenuation( - bad_input - ) diff --git a/tests/common/general_maths/test_transmission_from_attenuation.py b/tests/common/general_maths/test_transmission_from_attenuation.py deleted file mode 100644 index 75370b9c84a..00000000000 --- a/tests/common/general_maths/test_transmission_from_attenuation.py +++ /dev/null @@ -1,39 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths import transmission_interconversion - - -# happy: -@pytest.mark.parametrize( - "attenuation_bn,result", - [ - (0, 1), # tests transparency from zero attenuation (canonical) - ( - 1e3, - 0.3678794, - ), # tests transmission from canonical attenuation is 1/e (canonical) - ( - 145.1, - 0.8649358, - ), # tests transmission from arbitrary weak attenuation is -1000*attenuation - ( - 7221.9, - 7.3041331435179e-4, - ), # tests transmission from arbitrary strong attenuation is -1000*attenuation - ], -) -def test_transmission_from_attenutation(attenuation_bn, result): - assert transmission_interconversion.transmission_from_attenutation( - attenuation_bn - ) == pytest.approx(result) - - -# inauspicious: - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_transmission_from_attenutation_raises_error(bad_input): - with pytest.raises(TypeError): - transmission_interconversion.transmission_from_attenutation(bad_input) diff --git a/tests/common/general_maths/test_transmission_inerconversion.py b/tests/common/general_maths/test_transmission_inerconversion.py new file mode 100644 index 00000000000..2e62f386009 --- /dev/null +++ b/tests/common/general_maths/test_transmission_inerconversion.py @@ -0,0 +1,124 @@ +import math + +import pytest + +from dodal.common.general_maths.transmission_interconversion import ( + attenuation_from_natural_log_of_transmission, + attenuation_from_transmission, + natural_log_of_transmission_from_attenuation, + transmission_from_attenutation, +) + + +@pytest.mark.parametrize( + "attenuation_bn,result", + [ + (0, 1), # tests transparency from zero attenuation (canonical) + ( + 1e3, + 0.3678794, + ), # tests transmission from canonical attenuation is 1/e (canonical) + ( + 145.1, + 0.8649358, + ), # tests transmission from arbitrary weak attenuation is -1000*attenuation + ( + 7221.9, + 7.3041331435179e-4, + ), # tests transmission from arbitrary strong attenuation is -1000*attenuation + ], +) +def test_transmission_from_attenutation(attenuation_bn, result): + assert transmission_from_attenutation(attenuation_bn) == pytest.approx(result) + + +@pytest.mark.parametrize( + "attenuation_bn,result", + [ + (0, 0), # tests natural log of transparency from attenuation (canonical) + ( + 1e3, + -1, + ), # tests natural log of transmission from canonical transmission is negative + # unity (canonical) + ( + 712.6, + -0.7126, + ), # tests attenuation from natural log arbitrary high transmission is -1000 * + # attenuation + ( + 8034.1, + -8.0341, + ), # tests attenuation from natural log arbitrary low transmission is -1000 * + # attenuation + ], +) +def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): + assert natural_log_of_transmission_from_attenuation( + attenuation_bn + ) == pytest.approx(result) + + +@pytest.mark.parametrize( + "transmission_as_fraction,result", + [ + (1, 0), # tests attenuation from transparency is zero (canonical) + (0.37, 994.25227334), # tests attenuation from 37% is close to 1000 (canonical) + ( + 0.871, + 138.1133, + ), # tests attenuation from arbitrary high transmission has expected value + ( + 4.2e-4, + 7775.2558, + ), # tests attenuation from arbitrary high attenuation has expected value + ], +) +def test_attenuation_from_transmission(transmission_as_fraction, result): + assert attenuation_from_transmission(transmission_as_fraction) == pytest.approx( + result + ) + + +@pytest.mark.parametrize( + "ln_t,result", + [ + (-1, 1000), # tests negative unity log of transmission is 1000 (canonical) + (0, 0), # tests natural log of transparency is zero (canonical) + ( + -0.4367, + 436.7, + ), # tests log from arbitrary high transmission is -1000 * log + ( + -5.9017, + 5901.7, + ), # tests log from arbitrary low transmission is -1000 * log + ], +) +def test_attenuation_from_natural_log_of_transmission(ln_t, result): + assert attenuation_from_natural_log_of_transmission(ln_t) == pytest.approx(result) + + +# inauspicious: +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_natural_log_of_transmission_from_attenuation_raises_error(bad_input): + with pytest.raises(TypeError): + natural_log_of_transmission_from_attenuation(bad_input) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_transmission_from_attenutation_raises_error(bad_input): + with pytest.raises(TypeError): + transmission_from_attenutation(bad_input) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_from_transmission_raises_error(bad_input): + with pytest.raises(TypeError): + attenuation_from_transmission(bad_input) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_from_natural_log_of_transmission_raises_error(bad_input): + with pytest.raises(TypeError): + attenuation_from_natural_log_of_transmission(bad_input) From 788ad002f157a4cc3b6f60cbc9eeedb2a819a033 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 11:42:28 +0100 Subject: [PATCH 15/28] rejoined tests --- .../test_attenuation_at_depth.py | 55 ------- .../test_material_absorption_maths.py | 137 ++++++++++++++++++ .../test_photon_mass_attenuation.py | 52 ------- .../test_thickness_required_to_attenuate.py | 48 ------ 4 files changed, 137 insertions(+), 155 deletions(-) delete mode 100644 tests/common/general_maths/test_attenuation_at_depth.py create mode 100644 tests/common/general_maths/test_material_absorption_maths.py delete mode 100644 tests/common/general_maths/test_photon_mass_attenuation.py delete mode 100644 tests/common/general_maths/test_thickness_required_to_attenuate.py diff --git a/tests/common/general_maths/test_attenuation_at_depth.py b/tests/common/general_maths/test_attenuation_at_depth.py deleted file mode 100644 index bb5d1428deb..00000000000 --- a/tests/common/general_maths/test_attenuation_at_depth.py +++ /dev/null @@ -1,55 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths import material_absorption_maths - - -# happy path -@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 material_absorption_maths.attenuation_at_depth_cm( - depth_cm, absorption_coefficient_per_cm - ) == pytest.approx(result, rel=1e-6) - - -# inauspicious path - - -@pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) -def test_attenuation_at_depth_cm_absorption_coefficient_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.attenuation_at_depth_cm(1.0, bad_input) - - -@pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) -def test_attenuation_at_depth_cm_depth_raises_negative_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.attenuation_at_depth_cm(bad_input, 1.0) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_at_depth_cm_depth_depth_raises_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.attenuation_at_depth_cm(bad_input, 1.0) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_at_depth_cm_depth_absorption_raises_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.attenuation_at_depth_cm(1.0, bad_input) diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py new file mode 100644 index 00000000000..452268c4d3b --- /dev/null +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -0,0 +1,137 @@ +import math + +import pytest + +from dodal.common.general_maths.material_absorption_maths import ( + attenuation_at_depth_cm, + photon_mass_attenuation_per_unit_length, + thickness_cm_required_to_attenuate, +) + + +# happy path +@pytest.mark.parametrize( + "energy_kev,photon_absorption_factor_per_unit_length,energy_dependence_exponent," + "result", + [ + (5.042, 1.98e2, -2.717, 2.44170544), # Arbitrary + (8.3328, 2.5706e3, -2.83, 6.3708311), # Nickel + (11.9187, 1.48e3, -2.93, 1.03970725), # Gold-Three + (25.514, 6.48e3, -2.41, 2.63778077), # Silver + ], +) +def test_photon_mass_attenuation_per_unit_length( + energy_kev, + photon_absorption_factor_per_unit_length, + energy_dependence_exponent, + result, +): + assert photon_mass_attenuation_per_unit_length( + energy_kev, photon_absorption_factor_per_unit_length, energy_dependence_exponent + ) == 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 +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_energy(bad_input): + with pytest.raises(TypeError): + photon_mass_attenuation_per_unit_length(bad_input, 1.0, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_absorp(bad_input): + with pytest.raises(TypeError): + photon_mass_attenuation_per_unit_length(1.0, bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_expo(bad_input): + with pytest.raises(TypeError): + photon_mass_attenuation_per_unit_length(1.0, 1.0, bad_input) + + +def test_thickness_cm_required_to_attenuate_too_small_coefficient(): + with pytest.raises(ValueError): + thickness_cm_required_to_attenuate(1, 1e-15) + + +def test_thickness_required_to_attenuate_raises_negative_error(): + with pytest.raises(ValueError): + thickness_cm_required_to_attenuate(-1, 1) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_thickness_cm_required_to_attenuate_target_raises_error(bad_input): + with pytest.raises(ValueError): + thickness_cm_required_to_attenuate(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_thickness_cm_required_to_attenuate_absorption_raises_error(bad_input): + with pytest.raises(ValueError): + thickness_cm_required_to_attenuate(1.0, bad_input) + + +@pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) +def test_attenuation_at_depth_cm_absorption_coefficient_error(bad_input): + with pytest.raises(ValueError): + attenuation_at_depth_cm(1.0, bad_input) + + +@pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) +def test_attenuation_at_depth_cm_depth_raises_negative_error(bad_input): + with pytest.raises(ValueError): + attenuation_at_depth_cm(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_at_depth_cm_depth_depth_raises_error(bad_input): + with pytest.raises(ValueError): + attenuation_at_depth_cm(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_at_depth_cm_depth_absorption_raises_error(bad_input): + with pytest.raises(ValueError): + attenuation_at_depth_cm(1.0, bad_input) diff --git a/tests/common/general_maths/test_photon_mass_attenuation.py b/tests/common/general_maths/test_photon_mass_attenuation.py deleted file mode 100644 index a5647372ede..00000000000 --- a/tests/common/general_maths/test_photon_mass_attenuation.py +++ /dev/null @@ -1,52 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths import material_absorption_maths - - -# happy path -@pytest.mark.parametrize( - "energy_kev,photon_absorption_factor_per_unit_length,energy_dependence_exponent," - "result", - [ - (5.042, 1.98e2, -2.717, 2.44170544), # Arbitrary - (8.3328, 2.5706e3, -2.83, 6.3708311), # Nickel - (11.9187, 1.48e3, -2.93, 1.03970725), # Gold-Three - (25.514, 6.48e3, -2.41, 2.63778077), # Silver - ], -) -def test_photon_mass_attenuation_per_unit_length( - energy_kev, - photon_absorption_factor_per_unit_length, - energy_dependence_exponent, - result, -): - assert material_absorption_maths.photon_mass_attenuation_per_unit_length( - energy_kev, photon_absorption_factor_per_unit_length, energy_dependence_exponent - ) == pytest.approx(result) - - -# inauspicious path -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_photon_mass_attenuation_per_unit_length_errors_energy(bad_input): - with pytest.raises(TypeError): - material_absorption_maths.photon_mass_attenuation_per_unit_length( - bad_input, 1.0, 1.0 - ) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_photon_mass_attenuation_per_unit_length_errors_absorp(bad_input): - with pytest.raises(TypeError): - material_absorption_maths.photon_mass_attenuation_per_unit_length( - 1.0, bad_input, 1.0 - ) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_photon_mass_attenuation_per_unit_length_errors_expo(bad_input): - with pytest.raises(TypeError): - material_absorption_maths.photon_mass_attenuation_per_unit_length( - 1.0, 1.0, bad_input - ) diff --git a/tests/common/general_maths/test_thickness_required_to_attenuate.py b/tests/common/general_maths/test_thickness_required_to_attenuate.py deleted file mode 100644 index 4c47a1f42ec..00000000000 --- a/tests/common/general_maths/test_thickness_required_to_attenuate.py +++ /dev/null @@ -1,48 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths import material_absorption_maths - - -# happy path -@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 material_absorption_maths.thickness_cm_required_to_attenuate( - target_attenuation_bn, absorption_coefficient_per_cm - ) == pytest.approx(result, rel=1e-6) - - -# inauspicious path -def test_thickness_cm_required_to_attenuate_too_small_coefficient(): - with pytest.raises(ValueError): - material_absorption_maths.thickness_cm_required_to_attenuate(1, 1e-15) - - -def test_thickness_required_to_attenuate_raises_negative_error(): - with pytest.raises(ValueError): - material_absorption_maths.thickness_cm_required_to_attenuate(-1, 1) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_thickness_cm_required_to_attenuate_target_raises_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.thickness_cm_required_to_attenuate(bad_input, 1.0) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_thickness_cm_required_to_attenuate_absorption_raises_error(bad_input): - with pytest.raises(ValueError): - material_absorption_maths.thickness_cm_required_to_attenuate(1.0, bad_input) From 3ce24717c3be7c3a5c2cc4825f158ab19b7c4c84 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 15:27:59 +0100 Subject: [PATCH 16/28] updates based on review --- .../general_maths/material_absorption_maths.py | 6 +++--- .../test_material_absorption_maths.py | 17 ++++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py index 23eaf723ab2..6c133d21217 100644 --- a/src/dodal/common/general_maths/material_absorption_maths.py +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -28,11 +28,11 @@ def photon_mass_attenuation_per_unit_length( def attenuation_at_depth_cm( depth_cm: NonNegativeFloat, absorption_coefficient_per_cm: NonNegativeFloat ): - # TODO: check this - not sure if it really is the depth of the wedge? - """Calculates attenuation in Barnett units. + """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 (float): depth of wedge + depth_cm (float): depth of absorption absorption_coefficient_per_cm (float): absorption coefficient per cm Raises: diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py index 452268c4d3b..795ad32713e 100644 --- a/tests/common/general_maths/test_material_absorption_maths.py +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -74,26 +74,29 @@ def test_attenuation_at_depth_cm(depth_cm, absorption_coefficient_per_cm, result # inauspicious path @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_photon_mass_attenuation_per_unit_length_errors_energy(bad_input): +def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_energy(bad_input): with pytest.raises(TypeError): photon_mass_attenuation_per_unit_length(bad_input, 1.0, 1.0) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_photon_mass_attenuation_per_unit_length_errors_absorp(bad_input): +def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_factor(bad_input): with pytest.raises(TypeError): - photon_mass_attenuation_per_unit_length(1.0, bad_input, 1.0) + photon_mass_attenuation_per_unit_length(3500.0, bad_input, 1.0) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_photon_mass_attenuation_per_unit_length_errors_expo(bad_input): +def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_exponent( + bad_input, +): with pytest.raises(TypeError): - photon_mass_attenuation_per_unit_length(1.0, 1.0, bad_input) + photon_mass_attenuation_per_unit_length(3500.0, 1.0, bad_input) -def test_thickness_cm_required_to_attenuate_too_small_coefficient(): +def test_thickness_cm_required_to_attenuate_with_transparent_medium(): with pytest.raises(ValueError): - thickness_cm_required_to_attenuate(1, 1e-15) + transparent_medium = 1.0e-15 + thickness_cm_required_to_attenuate(3500.0, transparent_medium) def test_thickness_required_to_attenuate_raises_negative_error(): From f1c30c5f540bb3c685f13cc049e528d6ba1bf831 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 15:37:25 +0100 Subject: [PATCH 17/28] renames test --- .../test_material_absorption_maths.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py index 795ad32713e..f548130a66c 100644 --- a/tests/common/general_maths/test_material_absorption_maths.py +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -99,42 +99,46 @@ def test_thickness_cm_required_to_attenuate_with_transparent_medium(): thickness_cm_required_to_attenuate(3500.0, transparent_medium) -def test_thickness_required_to_attenuate_raises_negative_error(): +def test_thickness_required_to_attenuate_raises_error_for_gain(): with pytest.raises(ValueError): thickness_cm_required_to_attenuate(-1, 1) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_thickness_cm_required_to_attenuate_target_raises_error(bad_input): +def test_thickness_required_to_attenuate_raises_error_with_invalid_target_attenuation( + bad_input, +): with pytest.raises(ValueError): thickness_cm_required_to_attenuate(bad_input, 1.0) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_thickness_cm_required_to_attenuate_absorption_raises_error(bad_input): +def test_thickness_required_to_attenuate_raises_error_with_invalid_absorption( + bad_input, +): with pytest.raises(ValueError): thickness_cm_required_to_attenuate(1.0, bad_input) @pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) -def test_attenuation_at_depth_cm_absorption_coefficient_error(bad_input): +def test_attenuation_at_depth_raises_error_with_invalid_absorption(bad_input): with pytest.raises(ValueError): attenuation_at_depth_cm(1.0, bad_input) @pytest.mark.parametrize("bad_input", [-1, -5, -0.1]) -def test_attenuation_at_depth_cm_depth_raises_negative_error(bad_input): +def test_attenuation_at_depth_raises_error_for_unphysical_depths(bad_input): with pytest.raises(ValueError): attenuation_at_depth_cm(bad_input, 1.0) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_at_depth_cm_depth_depth_raises_error(bad_input): +def test_attenuation_at_depth_raises_error_with_invalid_depth(bad_input): with pytest.raises(ValueError): attenuation_at_depth_cm(bad_input, 1.0) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) -def test_attenuation_at_depth_cm_depth_absorption_raises_error(bad_input): +def test_attenuation_at_depth_raises_error_with_invalid_attenuation(bad_input): with pytest.raises(ValueError): attenuation_at_depth_cm(1.0, bad_input) From 1fd5e87b0a0410d937b84f4fa812a72e87871bf8 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 15:48:05 +0100 Subject: [PATCH 18/28] fixed more comments --- .../test_transmission_inerconversion.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/common/general_maths/test_transmission_inerconversion.py b/tests/common/general_maths/test_transmission_inerconversion.py index 2e62f386009..bf4d35d0858 100644 --- a/tests/common/general_maths/test_transmission_inerconversion.py +++ b/tests/common/general_maths/test_transmission_inerconversion.py @@ -21,11 +21,11 @@ ( 145.1, 0.8649358, - ), # tests transmission from arbitrary weak attenuation is -1000*attenuation + ), # tests transmission from arbitrary weak attenuation is weak attenuation ( 7221.9, 7.3041331435179e-4, - ), # tests transmission from arbitrary strong attenuation is -1000*attenuation + ), # tests transmission from arbitrary strong attenuation is strong attenuation ], ) def test_transmission_from_attenutation(attenuation_bn, result): @@ -44,13 +44,11 @@ def test_transmission_from_attenutation(attenuation_bn, result): ( 712.6, -0.7126, - ), # tests attenuation from natural log arbitrary high transmission is -1000 * - # attenuation + ), # tests attenuation from natural log arbitrary high transmission ( 8034.1, -8.0341, - ), # tests attenuation from natural log arbitrary low transmission is -1000 * - # attenuation + ), # tests attenuation from natural log arbitrary low transmission ], ) def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): @@ -63,15 +61,15 @@ def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): "transmission_as_fraction,result", [ (1, 0), # tests attenuation from transparency is zero (canonical) - (0.37, 994.25227334), # tests attenuation from 37% is close to 1000 (canonical) + (0.37, 994.25227334), # tests attenuation from 37% is close to 1000 ( 0.871, 138.1133, - ), # tests attenuation from arbitrary high transmission has expected value + ), # tests attenuation from arbitrary high transmission ( 4.2e-4, 7775.2558, - ), # tests attenuation from arbitrary high attenuation has expected value + ), # tests attenuation from arbitrary high attenuation ], ) def test_attenuation_from_transmission(transmission_as_fraction, result): From 7aa99416dc3200e7648e0038d4219a69898a79e5 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 15:49:08 +0100 Subject: [PATCH 19/28] fixed more comments --- .../common/general_maths/test_transmission_inerconversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/common/general_maths/test_transmission_inerconversion.py b/tests/common/general_maths/test_transmission_inerconversion.py index bf4d35d0858..781ff58e7db 100644 --- a/tests/common/general_maths/test_transmission_inerconversion.py +++ b/tests/common/general_maths/test_transmission_inerconversion.py @@ -86,11 +86,11 @@ def test_attenuation_from_transmission(transmission_as_fraction, result): ( -0.4367, 436.7, - ), # tests log from arbitrary high transmission is -1000 * log + ), # tests log from arbitrary high transmission ( -5.9017, 5901.7, - ), # tests log from arbitrary low transmission is -1000 * log + ), # tests log from arbitrary low transmission ], ) def test_attenuation_from_natural_log_of_transmission(ln_t, result): From b5e66635f90897ca5685fcac04380a9a7edafbd2 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 15:54:24 +0100 Subject: [PATCH 20/28] adds bools to bad input --- .../test_material_absorption_maths.py | 14 +++++++------- .../test_transmission_inerconversion.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py index f548130a66c..0e9258ddbc0 100644 --- a/tests/common/general_maths/test_material_absorption_maths.py +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -73,19 +73,19 @@ def test_attenuation_at_depth_cm(depth_cm, absorption_coefficient_per_cm, result # inauspicious path -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_energy(bad_input): with pytest.raises(TypeError): photon_mass_attenuation_per_unit_length(bad_input, 1.0, 1.0) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_factor(bad_input): with pytest.raises(TypeError): photon_mass_attenuation_per_unit_length(3500.0, bad_input, 1.0) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_exponent( bad_input, ): @@ -104,7 +104,7 @@ def test_thickness_required_to_attenuate_raises_error_for_gain(): thickness_cm_required_to_attenuate(-1, 1) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@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, ): @@ -112,7 +112,7 @@ def test_thickness_required_to_attenuate_raises_error_with_invalid_target_attenu thickness_cm_required_to_attenuate(bad_input, 1.0) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_thickness_required_to_attenuate_raises_error_with_invalid_absorption( bad_input, ): @@ -132,13 +132,13 @@ def test_attenuation_at_depth_raises_error_for_unphysical_depths(bad_input): attenuation_at_depth_cm(bad_input, 1.0) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@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(ValueError): attenuation_at_depth_cm(bad_input, 1.0) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@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(ValueError): attenuation_at_depth_cm(1.0, bad_input) diff --git a/tests/common/general_maths/test_transmission_inerconversion.py b/tests/common/general_maths/test_transmission_inerconversion.py index 781ff58e7db..2629f00dcfe 100644 --- a/tests/common/general_maths/test_transmission_inerconversion.py +++ b/tests/common/general_maths/test_transmission_inerconversion.py @@ -98,25 +98,25 @@ def test_attenuation_from_natural_log_of_transmission(ln_t, result): # inauspicious: -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_natural_log_of_transmission_from_attenuation_raises_error(bad_input): with pytest.raises(TypeError): natural_log_of_transmission_from_attenuation(bad_input) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_transmission_from_attenutation_raises_error(bad_input): with pytest.raises(TypeError): transmission_from_attenutation(bad_input) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_attenuation_from_transmission_raises_error(bad_input): with pytest.raises(TypeError): attenuation_from_transmission(bad_input) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_attenuation_from_natural_log_of_transmission_raises_error(bad_input): with pytest.raises(TypeError): attenuation_from_natural_log_of_transmission(bad_input) From 28fce46c7e353f1c9b295d79c2c2d55899cf6311 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 15:56:00 +0100 Subject: [PATCH 21/28] fixed based on comments in 2041 --- .../material_absorption_maths.py | 82 ---------- .../test_material_absorption_maths.py | 144 ------------------ 2 files changed, 226 deletions(-) delete mode 100644 src/dodal/common/general_maths/material_absorption_maths.py delete mode 100644 tests/common/general_maths/test_material_absorption_maths.py diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py deleted file mode 100644 index 6c133d21217..00000000000 --- a/src/dodal/common/general_maths/material_absorption_maths.py +++ /dev/null @@ -1,82 +0,0 @@ -from pydantic import NonNegativeFloat, validate_call - -from dodal.common.general_maths import transmission_interconversion - - -def photon_mass_attenuation_per_unit_length( - energy_kev: float, - photon_absorption_factor_per_unit_length: float, - energy_dependence_exponent: float, -): - """Calculates mass attenuation per unit length. - - Args: - energy_kev (float): energy - photon_absorption_factor_per_unit_length (float): photon absorption factor per - unit length - energy_dependence_exponent (float): energy dependence exponent - - Returns: - (float): mass attenuation per unit length. - """ - return photon_absorption_factor_per_unit_length * ( - energy_kev**energy_dependence_exponent - ) - - -@validate_call() -def attenuation_at_depth_cm( - depth_cm: NonNegativeFloat, absorption_coefficient_per_cm: NonNegativeFloat -): - """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 (float): depth of absorption - absorption_coefficient_per_cm (float): 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 - """ - ln_t = -(depth_cm * absorption_coefficient_per_cm) - return transmission_interconversion.attenuation_from_natural_log_of_transmission( - ln_t - ) - - -@validate_call() -def thickness_cm_required_to_attenuate( - target_attenuation_bn: NonNegativeFloat, - absorption_coefficient_per_cm: float, -): - """Calculates material depth in cm. - - Args: - target_attenuation_bn (float): Target attenuation to meet in Barnett attenuation - units. - absorption_coefficient_per_cm (float): 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 - 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." - ) - ln_target_t = ( - transmission_interconversion.natural_log_of_transmission_from_attenuation( - target_attenuation_bn - ) - ) - return -(ln_target_t / absorption_coefficient_per_cm) diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py deleted file mode 100644 index 0e9258ddbc0..00000000000 --- a/tests/common/general_maths/test_material_absorption_maths.py +++ /dev/null @@ -1,144 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths.material_absorption_maths import ( - attenuation_at_depth_cm, - photon_mass_attenuation_per_unit_length, - thickness_cm_required_to_attenuate, -) - - -# happy path -@pytest.mark.parametrize( - "energy_kev,photon_absorption_factor_per_unit_length,energy_dependence_exponent," - "result", - [ - (5.042, 1.98e2, -2.717, 2.44170544), # Arbitrary - (8.3328, 2.5706e3, -2.83, 6.3708311), # Nickel - (11.9187, 1.48e3, -2.93, 1.03970725), # Gold-Three - (25.514, 6.48e3, -2.41, 2.63778077), # Silver - ], -) -def test_photon_mass_attenuation_per_unit_length( - energy_kev, - photon_absorption_factor_per_unit_length, - energy_dependence_exponent, - result, -): - assert photon_mass_attenuation_per_unit_length( - energy_kev, photon_absorption_factor_per_unit_length, energy_dependence_exponent - ) == 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 -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) -def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_energy(bad_input): - with pytest.raises(TypeError): - photon_mass_attenuation_per_unit_length(bad_input, 1.0, 1.0) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) -def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_factor(bad_input): - with pytest.raises(TypeError): - photon_mass_attenuation_per_unit_length(3500.0, bad_input, 1.0) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) -def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_exponent( - bad_input, -): - with pytest.raises(TypeError): - 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(ValueError): - 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(ValueError): - 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(ValueError): - 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(ValueError): - 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(ValueError): - 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(ValueError): - 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(ValueError): - attenuation_at_depth_cm(1.0, bad_input) From a706c3a8e5e988a97ec87070b373111b064d8170 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 16:04:11 +0100 Subject: [PATCH 22/28] fixes spelling mistake --- .../test_transmission_inerconversion.py | 122 ------------------ 1 file changed, 122 deletions(-) delete mode 100644 tests/common/general_maths/test_transmission_inerconversion.py diff --git a/tests/common/general_maths/test_transmission_inerconversion.py b/tests/common/general_maths/test_transmission_inerconversion.py deleted file mode 100644 index 2629f00dcfe..00000000000 --- a/tests/common/general_maths/test_transmission_inerconversion.py +++ /dev/null @@ -1,122 +0,0 @@ -import math - -import pytest - -from dodal.common.general_maths.transmission_interconversion import ( - attenuation_from_natural_log_of_transmission, - attenuation_from_transmission, - natural_log_of_transmission_from_attenuation, - transmission_from_attenutation, -) - - -@pytest.mark.parametrize( - "attenuation_bn,result", - [ - (0, 1), # tests transparency from zero attenuation (canonical) - ( - 1e3, - 0.3678794, - ), # tests transmission from canonical attenuation is 1/e (canonical) - ( - 145.1, - 0.8649358, - ), # tests transmission from arbitrary weak attenuation is weak attenuation - ( - 7221.9, - 7.3041331435179e-4, - ), # tests transmission from arbitrary strong attenuation is strong attenuation - ], -) -def test_transmission_from_attenutation(attenuation_bn, result): - assert transmission_from_attenutation(attenuation_bn) == pytest.approx(result) - - -@pytest.mark.parametrize( - "attenuation_bn,result", - [ - (0, 0), # tests natural log of transparency from attenuation (canonical) - ( - 1e3, - -1, - ), # tests natural log of transmission from canonical transmission is negative - # unity (canonical) - ( - 712.6, - -0.7126, - ), # tests attenuation from natural log arbitrary high transmission - ( - 8034.1, - -8.0341, - ), # tests attenuation from natural log arbitrary low transmission - ], -) -def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): - assert natural_log_of_transmission_from_attenuation( - attenuation_bn - ) == pytest.approx(result) - - -@pytest.mark.parametrize( - "transmission_as_fraction,result", - [ - (1, 0), # tests attenuation from transparency is zero (canonical) - (0.37, 994.25227334), # tests attenuation from 37% is close to 1000 - ( - 0.871, - 138.1133, - ), # tests attenuation from arbitrary high transmission - ( - 4.2e-4, - 7775.2558, - ), # tests attenuation from arbitrary high attenuation - ], -) -def test_attenuation_from_transmission(transmission_as_fraction, result): - assert attenuation_from_transmission(transmission_as_fraction) == pytest.approx( - result - ) - - -@pytest.mark.parametrize( - "ln_t,result", - [ - (-1, 1000), # tests negative unity log of transmission is 1000 (canonical) - (0, 0), # tests natural log of transparency is zero (canonical) - ( - -0.4367, - 436.7, - ), # tests log from arbitrary high transmission - ( - -5.9017, - 5901.7, - ), # tests log from arbitrary low transmission - ], -) -def test_attenuation_from_natural_log_of_transmission(ln_t, result): - assert attenuation_from_natural_log_of_transmission(ln_t) == pytest.approx(result) - - -# inauspicious: -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) -def test_natural_log_of_transmission_from_attenuation_raises_error(bad_input): - with pytest.raises(TypeError): - natural_log_of_transmission_from_attenuation(bad_input) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) -def test_transmission_from_attenutation_raises_error(bad_input): - with pytest.raises(TypeError): - transmission_from_attenutation(bad_input) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) -def test_attenuation_from_transmission_raises_error(bad_input): - with pytest.raises(TypeError): - attenuation_from_transmission(bad_input) - - -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) -def test_attenuation_from_natural_log_of_transmission_raises_error(bad_input): - with pytest.raises(TypeError): - attenuation_from_natural_log_of_transmission(bad_input) From a20395ed9bc806ba6b1686fe88d16359b3b05183 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 16:05:12 +0100 Subject: [PATCH 23/28] changes name --- .../test_transmission_interconversion.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/common/general_maths/test_transmission_interconversion.py diff --git a/tests/common/general_maths/test_transmission_interconversion.py b/tests/common/general_maths/test_transmission_interconversion.py new file mode 100644 index 00000000000..2629f00dcfe --- /dev/null +++ b/tests/common/general_maths/test_transmission_interconversion.py @@ -0,0 +1,122 @@ +import math + +import pytest + +from dodal.common.general_maths.transmission_interconversion import ( + attenuation_from_natural_log_of_transmission, + attenuation_from_transmission, + natural_log_of_transmission_from_attenuation, + transmission_from_attenutation, +) + + +@pytest.mark.parametrize( + "attenuation_bn,result", + [ + (0, 1), # tests transparency from zero attenuation (canonical) + ( + 1e3, + 0.3678794, + ), # tests transmission from canonical attenuation is 1/e (canonical) + ( + 145.1, + 0.8649358, + ), # tests transmission from arbitrary weak attenuation is weak attenuation + ( + 7221.9, + 7.3041331435179e-4, + ), # tests transmission from arbitrary strong attenuation is strong attenuation + ], +) +def test_transmission_from_attenutation(attenuation_bn, result): + assert transmission_from_attenutation(attenuation_bn) == pytest.approx(result) + + +@pytest.mark.parametrize( + "attenuation_bn,result", + [ + (0, 0), # tests natural log of transparency from attenuation (canonical) + ( + 1e3, + -1, + ), # tests natural log of transmission from canonical transmission is negative + # unity (canonical) + ( + 712.6, + -0.7126, + ), # tests attenuation from natural log arbitrary high transmission + ( + 8034.1, + -8.0341, + ), # tests attenuation from natural log arbitrary low transmission + ], +) +def test_natural_log_of_transmission_from_attenuation(attenuation_bn, result): + assert natural_log_of_transmission_from_attenuation( + attenuation_bn + ) == pytest.approx(result) + + +@pytest.mark.parametrize( + "transmission_as_fraction,result", + [ + (1, 0), # tests attenuation from transparency is zero (canonical) + (0.37, 994.25227334), # tests attenuation from 37% is close to 1000 + ( + 0.871, + 138.1133, + ), # tests attenuation from arbitrary high transmission + ( + 4.2e-4, + 7775.2558, + ), # tests attenuation from arbitrary high attenuation + ], +) +def test_attenuation_from_transmission(transmission_as_fraction, result): + assert attenuation_from_transmission(transmission_as_fraction) == pytest.approx( + result + ) + + +@pytest.mark.parametrize( + "ln_t,result", + [ + (-1, 1000), # tests negative unity log of transmission is 1000 (canonical) + (0, 0), # tests natural log of transparency is zero (canonical) + ( + -0.4367, + 436.7, + ), # tests log from arbitrary high transmission + ( + -5.9017, + 5901.7, + ), # tests log from arbitrary low transmission + ], +) +def test_attenuation_from_natural_log_of_transmission(ln_t, result): + assert attenuation_from_natural_log_of_transmission(ln_t) == pytest.approx(result) + + +# inauspicious: +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) +def test_natural_log_of_transmission_from_attenuation_raises_error(bad_input): + with pytest.raises(TypeError): + natural_log_of_transmission_from_attenuation(bad_input) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) +def test_transmission_from_attenutation_raises_error(bad_input): + with pytest.raises(TypeError): + transmission_from_attenutation(bad_input) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) +def test_attenuation_from_transmission_raises_error(bad_input): + with pytest.raises(TypeError): + attenuation_from_transmission(bad_input) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) +def test_attenuation_from_natural_log_of_transmission_raises_error(bad_input): + with pytest.raises(TypeError): + attenuation_from_natural_log_of_transmission(bad_input) From fbbd27831473b56dae134be02e980fc2ee7cb52f Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 16:42:56 +0100 Subject: [PATCH 24/28] alters to make sure bool is rejected --- .../general_maths/transmission_interconversion.py | 14 ++++++++++---- .../test_transmission_interconversion.py | 9 +++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/dodal/common/general_maths/transmission_interconversion.py b/src/dodal/common/general_maths/transmission_interconversion.py index 70876148a9d..44d438a0fa8 100644 --- a/src/dodal/common/general_maths/transmission_interconversion.py +++ b/src/dodal/common/general_maths/transmission_interconversion.py @@ -1,10 +1,13 @@ import math +from pydantic import StrictFloat, validate_call + CANONICAL_BARNETT_CONVERSION = -1.0e3 REVERSE_BARNETT_CONVERSION = -1.0e-3 -def attenuation_from_natural_log_of_transmission(ln_t: float) -> float: +@validate_call +def attenuation_from_natural_log_of_transmission(ln_t: StrictFloat) -> float: """Converts from natural log of transmission fraction into Barnett attenuation units . @@ -17,7 +20,8 @@ def attenuation_from_natural_log_of_transmission(ln_t: float) -> float: return CANONICAL_BARNETT_CONVERSION * ln_t -def attenuation_from_transmission(transmission_as_fraction: float) -> float: +@validate_call +def attenuation_from_transmission(transmission_as_fraction: StrictFloat) -> float: """Converts from transmission fraction into Barnett attenuation units. Args: @@ -30,7 +34,8 @@ def attenuation_from_transmission(transmission_as_fraction: float) -> float: return attenuation_from_natural_log_of_transmission(ln_t) -def natural_log_of_transmission_from_attenuation(attenuation_bn: float) -> float: +@validate_call +def natural_log_of_transmission_from_attenuation(attenuation_bn: StrictFloat) -> float: """Converts from Barnett attenuation units into natural log of transmission fraction . @@ -43,7 +48,8 @@ def natural_log_of_transmission_from_attenuation(attenuation_bn: float) -> float return REVERSE_BARNETT_CONVERSION * attenuation_bn -def transmission_from_attenutation(attenuation_bn: float) -> float: +@validate_call +def transmission_from_attenutation(attenuation_bn: StrictFloat) -> float: """Converts from Barnett attenuation units into transmission fraction. Args: diff --git a/tests/common/general_maths/test_transmission_interconversion.py b/tests/common/general_maths/test_transmission_interconversion.py index 2629f00dcfe..5d28ed4b2eb 100644 --- a/tests/common/general_maths/test_transmission_interconversion.py +++ b/tests/common/general_maths/test_transmission_interconversion.py @@ -1,5 +1,6 @@ import math +import pydantic import pytest from dodal.common.general_maths.transmission_interconversion import ( @@ -100,23 +101,23 @@ def test_attenuation_from_natural_log_of_transmission(ln_t, result): # inauspicious: @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_natural_log_of_transmission_from_attenuation_raises_error(bad_input): - with pytest.raises(TypeError): + with pytest.raises(pydantic.ValidationError): natural_log_of_transmission_from_attenuation(bad_input) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_transmission_from_attenutation_raises_error(bad_input): - with pytest.raises(TypeError): + with pytest.raises(pydantic.ValidationError): transmission_from_attenutation(bad_input) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_attenuation_from_transmission_raises_error(bad_input): - with pytest.raises(TypeError): + with pytest.raises(pydantic.ValidationError): attenuation_from_transmission(bad_input) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_attenuation_from_natural_log_of_transmission_raises_error(bad_input): - with pytest.raises(TypeError): + with pytest.raises(pydantic.ValidationError): attenuation_from_natural_log_of_transmission(bad_input) From 72389b2666b705aa07857a823e6fcdcc7ca7a98d Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 16:51:57 +0100 Subject: [PATCH 25/28] accidentally deleted files, recovered. also ensures strictness in transmission_interconversion --- .../material_absorption_maths.py | 82 ++++++++++ .../test_material_absorption_maths.py | 144 ++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 src/dodal/common/general_maths/material_absorption_maths.py create mode 100644 tests/common/general_maths/test_material_absorption_maths.py diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py new file mode 100644 index 00000000000..6c133d21217 --- /dev/null +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -0,0 +1,82 @@ +from pydantic import NonNegativeFloat, validate_call + +from dodal.common.general_maths import transmission_interconversion + + +def photon_mass_attenuation_per_unit_length( + energy_kev: float, + photon_absorption_factor_per_unit_length: float, + energy_dependence_exponent: float, +): + """Calculates mass attenuation per unit length. + + Args: + energy_kev (float): energy + photon_absorption_factor_per_unit_length (float): photon absorption factor per + unit length + energy_dependence_exponent (float): energy dependence exponent + + Returns: + (float): mass attenuation per unit length. + """ + return photon_absorption_factor_per_unit_length * ( + energy_kev**energy_dependence_exponent + ) + + +@validate_call() +def attenuation_at_depth_cm( + depth_cm: NonNegativeFloat, absorption_coefficient_per_cm: NonNegativeFloat +): + """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 (float): depth of absorption + absorption_coefficient_per_cm (float): 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 + """ + ln_t = -(depth_cm * absorption_coefficient_per_cm) + return transmission_interconversion.attenuation_from_natural_log_of_transmission( + ln_t + ) + + +@validate_call() +def thickness_cm_required_to_attenuate( + target_attenuation_bn: NonNegativeFloat, + absorption_coefficient_per_cm: float, +): + """Calculates material depth in cm. + + Args: + target_attenuation_bn (float): Target attenuation to meet in Barnett attenuation + units. + absorption_coefficient_per_cm (float): 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 + 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." + ) + ln_target_t = ( + transmission_interconversion.natural_log_of_transmission_from_attenuation( + target_attenuation_bn + ) + ) + return -(ln_target_t / absorption_coefficient_per_cm) diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py new file mode 100644 index 00000000000..f548130a66c --- /dev/null +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -0,0 +1,144 @@ +import math + +import pytest + +from dodal.common.general_maths.material_absorption_maths import ( + attenuation_at_depth_cm, + photon_mass_attenuation_per_unit_length, + thickness_cm_required_to_attenuate, +) + + +# happy path +@pytest.mark.parametrize( + "energy_kev,photon_absorption_factor_per_unit_length,energy_dependence_exponent," + "result", + [ + (5.042, 1.98e2, -2.717, 2.44170544), # Arbitrary + (8.3328, 2.5706e3, -2.83, 6.3708311), # Nickel + (11.9187, 1.48e3, -2.93, 1.03970725), # Gold-Three + (25.514, 6.48e3, -2.41, 2.63778077), # Silver + ], +) +def test_photon_mass_attenuation_per_unit_length( + energy_kev, + photon_absorption_factor_per_unit_length, + energy_dependence_exponent, + result, +): + assert photon_mass_attenuation_per_unit_length( + energy_kev, photon_absorption_factor_per_unit_length, energy_dependence_exponent + ) == 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 +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_energy(bad_input): + with pytest.raises(TypeError): + photon_mass_attenuation_per_unit_length(bad_input, 1.0, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_factor(bad_input): + with pytest.raises(TypeError): + photon_mass_attenuation_per_unit_length(3500.0, bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_exponent( + bad_input, +): + with pytest.raises(TypeError): + 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(ValueError): + thickness_cm_required_to_attenuate(-1, 1) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_thickness_required_to_attenuate_raises_error_with_invalid_target_attenuation( + bad_input, +): + with pytest.raises(ValueError): + thickness_cm_required_to_attenuate(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_thickness_required_to_attenuate_raises_error_with_invalid_absorption( + bad_input, +): + with pytest.raises(ValueError): + 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(ValueError): + 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(ValueError): + attenuation_at_depth_cm(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_at_depth_raises_error_with_invalid_depth(bad_input): + with pytest.raises(ValueError): + attenuation_at_depth_cm(bad_input, 1.0) + + +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +def test_attenuation_at_depth_raises_error_with_invalid_attenuation(bad_input): + with pytest.raises(ValueError): + attenuation_at_depth_cm(1.0, bad_input) From 8be2371e9eb5f48ea9fbc655b2125c0173647e57 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 16:56:09 +0100 Subject: [PATCH 26/28] work in progress. need to create a basemodel --- .../material_absorption_maths.py | 30 +++++++++---------- .../transmission_interconversion.py | 8 ++--- .../test_material_absorption_maths.py | 14 ++++----- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py index 6c133d21217..f2ea4f35c7d 100644 --- a/src/dodal/common/general_maths/material_absorption_maths.py +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -1,20 +1,20 @@ -from pydantic import NonNegativeFloat, validate_call +from pydantic import NonNegativeFloat, StrictFloat, validate_call from dodal.common.general_maths import transmission_interconversion def photon_mass_attenuation_per_unit_length( - energy_kev: float, - photon_absorption_factor_per_unit_length: float, - energy_dependence_exponent: float, -): + energy_kev: StrictFloat, + photon_absorption_factor_per_unit_length: StrictFloat, + energy_dependence_exponent: StrictFloat, +) -> float: """Calculates mass attenuation per unit length. Args: - energy_kev (float): energy - photon_absorption_factor_per_unit_length (float): photon absorption factor per - unit length - energy_dependence_exponent (float): energy dependence exponent + energy_kev (StrictFloat): energy + 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. @@ -27,7 +27,7 @@ def photon_mass_attenuation_per_unit_length( @validate_call() def attenuation_at_depth_cm( depth_cm: NonNegativeFloat, absorption_coefficient_per_cm: NonNegativeFloat -): +) -> float: """Calculates attenuation in Barnett units, where 1000 Bn equivalent to 1/e, 0Bn to 1 and 2000 Bn to 1/(e^2). @@ -51,14 +51,14 @@ def attenuation_at_depth_cm( @validate_call() def thickness_cm_required_to_attenuate( target_attenuation_bn: NonNegativeFloat, - absorption_coefficient_per_cm: float, -): + absorption_coefficient_per_cm: StrictFloat, +) -> float: """Calculates material depth in cm. Args: - target_attenuation_bn (float): Target attenuation to meet in Barnett attenuation - units. - absorption_coefficient_per_cm (float): absorption coefficient per cm + target_attenuation_bn (NonNegativeFloat): Target attenuation to meet in Barnett + attenuation units. + absorption_coefficient_per_cm (StrictFloat): absorption coefficient per cm Raises: diff --git a/src/dodal/common/general_maths/transmission_interconversion.py b/src/dodal/common/general_maths/transmission_interconversion.py index 44d438a0fa8..725fdfb262b 100644 --- a/src/dodal/common/general_maths/transmission_interconversion.py +++ b/src/dodal/common/general_maths/transmission_interconversion.py @@ -12,7 +12,7 @@ def attenuation_from_natural_log_of_transmission(ln_t: StrictFloat) -> float: . Args: - ln_t (float): natural log of transmission fraction + ln_t (StrictFloat): natural log of transmission fraction Returns: (float): Barnett attenuation units @@ -25,7 +25,7 @@ def attenuation_from_transmission(transmission_as_fraction: StrictFloat) -> floa """Converts from transmission fraction into Barnett attenuation units. Args: - transmission_as_fraction (float): transmission fraction + transmission_as_fraction (StrictFloat): transmission fraction Returns: (float): Barnett attenuation units. @@ -40,7 +40,7 @@ def natural_log_of_transmission_from_attenuation(attenuation_bn: StrictFloat) -> . Args: - attenuation_bn (float): Barnett attenuation units + attenuation_bn (StrictFloat): Barnett attenuation units Returns: (float): natural log of transmission fraction @@ -53,7 +53,7 @@ def transmission_from_attenutation(attenuation_bn: StrictFloat) -> float: """Converts from Barnett attenuation units into transmission fraction. Args: - attenuation_bn (float): Barnett attenuation units + attenuation_bn (StrictFloat): Barnett attenuation units Returns: (float): transmission fraction diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py index f548130a66c..0e9258ddbc0 100644 --- a/tests/common/general_maths/test_material_absorption_maths.py +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -73,19 +73,19 @@ def test_attenuation_at_depth_cm(depth_cm, absorption_coefficient_per_cm, result # inauspicious path -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_energy(bad_input): with pytest.raises(TypeError): photon_mass_attenuation_per_unit_length(bad_input, 1.0, 1.0) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_factor(bad_input): with pytest.raises(TypeError): photon_mass_attenuation_per_unit_length(3500.0, bad_input, 1.0) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_exponent( bad_input, ): @@ -104,7 +104,7 @@ def test_thickness_required_to_attenuate_raises_error_for_gain(): thickness_cm_required_to_attenuate(-1, 1) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@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, ): @@ -112,7 +112,7 @@ def test_thickness_required_to_attenuate_raises_error_with_invalid_target_attenu thickness_cm_required_to_attenuate(bad_input, 1.0) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_thickness_required_to_attenuate_raises_error_with_invalid_absorption( bad_input, ): @@ -132,13 +132,13 @@ def test_attenuation_at_depth_raises_error_for_unphysical_depths(bad_input): attenuation_at_depth_cm(bad_input, 1.0) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@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(ValueError): attenuation_at_depth_cm(bad_input, 1.0) -@pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object()]) +@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(ValueError): attenuation_at_depth_cm(1.0, bad_input) From 60004d6dd1517145088bf23023851398611d3bb3 Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Wed, 13 May 2026 17:06:34 +0100 Subject: [PATCH 27/28] need to fix up non-negative floats but otherwise all up to date --- .../material_absorption_maths.py | 5 ++-- .../test_material_absorption_maths.py | 23 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py index f2ea4f35c7d..3ec53b34bc9 100644 --- a/src/dodal/common/general_maths/material_absorption_maths.py +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -3,6 +3,7 @@ from dodal.common.general_maths import transmission_interconversion +@validate_call def photon_mass_attenuation_per_unit_length( energy_kev: StrictFloat, photon_absorption_factor_per_unit_length: StrictFloat, @@ -24,7 +25,7 @@ def photon_mass_attenuation_per_unit_length( ) -@validate_call() +@validate_call def attenuation_at_depth_cm( depth_cm: NonNegativeFloat, absorption_coefficient_per_cm: NonNegativeFloat ) -> float: @@ -48,7 +49,7 @@ def attenuation_at_depth_cm( ) -@validate_call() +@validate_call def thickness_cm_required_to_attenuate( target_attenuation_bn: NonNegativeFloat, absorption_coefficient_per_cm: StrictFloat, diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py index 0e9258ddbc0..79c5affa231 100644 --- a/tests/common/general_maths/test_material_absorption_maths.py +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -1,6 +1,7 @@ import math import pytest +from pydantic import ValidationError from dodal.common.general_maths.material_absorption_maths import ( attenuation_at_depth_cm, @@ -75,13 +76,13 @@ def test_attenuation_at_depth_cm(depth_cm, absorption_coefficient_per_cm, result # inauspicious path @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_energy(bad_input): - with pytest.raises(TypeError): + with pytest.raises(ValidationError): photon_mass_attenuation_per_unit_length(bad_input, 1.0, 1.0) @pytest.mark.parametrize("bad_input", ["a", [], None, math.sin, object(), False]) def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_factor(bad_input): - with pytest.raises(TypeError): + with pytest.raises(ValidationError): photon_mass_attenuation_per_unit_length(3500.0, bad_input, 1.0) @@ -89,18 +90,18 @@ def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_factor(bad_ def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_exponent( bad_input, ): - with pytest.raises(TypeError): + 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): + with pytest.raises(ValidationError): 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(ValueError): + with pytest.raises(ValidationError): thickness_cm_required_to_attenuate(-1, 1) @@ -108,7 +109,7 @@ def test_thickness_required_to_attenuate_raises_error_for_gain(): def test_thickness_required_to_attenuate_raises_error_with_invalid_target_attenuation( bad_input, ): - with pytest.raises(ValueError): + with pytest.raises(ValidationError): thickness_cm_required_to_attenuate(bad_input, 1.0) @@ -116,29 +117,29 @@ def test_thickness_required_to_attenuate_raises_error_with_invalid_target_attenu def test_thickness_required_to_attenuate_raises_error_with_invalid_absorption( bad_input, ): - with pytest.raises(ValueError): + 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(ValueError): + 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(ValueError): + 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(ValueError): + 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(ValueError): + with pytest.raises(ValidationError): attenuation_at_depth_cm(1.0, bad_input) From 7b3c237ca1c407fcf2b627dce36f20f4ff06f8be Mon Sep 17 00:00:00 2001 From: Matthew Carre Date: Thu, 14 May 2026 10:20:39 +0100 Subject: [PATCH 28/28] adds a base model to check an input value. Changed back to strictfloats, but those that are relevant are forced to be greater than 0 --- .../material_absorption_maths.py | 41 ++++++++++++------- .../test_material_absorption_maths.py | 2 +- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/dodal/common/general_maths/material_absorption_maths.py b/src/dodal/common/general_maths/material_absorption_maths.py index 3ec53b34bc9..bd46f9e9983 100644 --- a/src/dodal/common/general_maths/material_absorption_maths.py +++ b/src/dodal/common/general_maths/material_absorption_maths.py @@ -1,6 +1,20 @@ -from pydantic import NonNegativeFloat, StrictFloat, validate_call +from typing import Annotated -from dodal.common.general_maths import transmission_interconversion +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 @@ -27,14 +41,14 @@ def photon_mass_attenuation_per_unit_length( @validate_call def attenuation_at_depth_cm( - depth_cm: NonNegativeFloat, absorption_coefficient_per_cm: NonNegativeFloat + 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 (float): depth of absorption - absorption_coefficient_per_cm (float): absorption coefficient per cm + 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 @@ -43,21 +57,21 @@ def attenuation_at_depth_cm( 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 transmission_interconversion.attenuation_from_natural_log_of_transmission( - ln_t - ) + return attenuation_from_natural_log_of_transmission(ln_t) @validate_call def thickness_cm_required_to_attenuate( - target_attenuation_bn: NonNegativeFloat, + target_attenuation_bn: StrictFloat, absorption_coefficient_per_cm: StrictFloat, ) -> float: """Calculates material depth in cm. Args: - target_attenuation_bn (NonNegativeFloat): Target attenuation to meet in Barnett + target_attenuation_bn (StrictFloat): Target attenuation to meet in Barnett attenuation units. absorption_coefficient_per_cm (StrictFloat): absorption coefficient per cm @@ -75,9 +89,6 @@ def thickness_cm_required_to_attenuate( "Invalid absorption - this calculator is not for transparent media nor thos\ e with optical gain." ) - ln_target_t = ( - transmission_interconversion.natural_log_of_transmission_from_attenuation( - target_attenuation_bn - ) - ) + 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) diff --git a/tests/common/general_maths/test_material_absorption_maths.py b/tests/common/general_maths/test_material_absorption_maths.py index 79c5affa231..bc892fe150f 100644 --- a/tests/common/general_maths/test_material_absorption_maths.py +++ b/tests/common/general_maths/test_material_absorption_maths.py @@ -95,7 +95,7 @@ def test_photon_mass_attenuation_per_unit_length_errors_with_invalid_exponent( def test_thickness_cm_required_to_attenuate_with_transparent_medium(): - with pytest.raises(ValidationError): + with pytest.raises(ValueError): transparent_medium = 1.0e-15 thickness_cm_required_to_attenuate(3500.0, transparent_medium)