Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9ed276e
initial commit
Matt-Carre May 1, 2026
7079d52
moved
Matt-Carre May 1, 2026
3300cef
working?
Matt-Carre May 1, 2026
e1d4b12
fixes last parts
Matt-Carre May 1, 2026
5014607
first draft complete
Matt-Carre May 1, 2026
9e14520
clears up some comments
Matt-Carre May 1, 2026
c260c91
Merge branch 'main' into 2038_basic_conversions
Matt-Carre May 1, 2026
f5e3e61
Merge branch 'main' into 2038_basic_conversions
Matt-Carre May 1, 2026
47e1ae1
fixes after first code review
Matt-Carre May 1, 2026
3be60a6
Merge branch '2038_basic_conversions' of github.com:DiamondLightSourc…
Matt-Carre May 1, 2026
e92ad39
splits testing up
Matt-Carre May 6, 2026
4cd0a9f
fixes most of comments, still need pydantic stuff
Matt-Carre May 6, 2026
3c86b4a
Adds pydantic validation (at last)
Matt-Carre May 7, 2026
7618002
edits error message
Matt-Carre May 7, 2026
e5461e9
Merge branch 'main' of github.com:DiamondLightSource/dodal into 2038_…
Matt-Carre May 11, 2026
782b147
Merge branch 'main' into 2038_basic_conversions
Matt-Carre May 13, 2026
032aa4d
Merge branch 'main' into 2038_basic_conversions
Matt-Carre May 13, 2026
7c3c57b
alters tests back to mirroring SRC file structure
Matt-Carre May 13, 2026
b2b5341
Merge branch '2038_basic_conversions' of github.com:DiamondLightSourc…
Matt-Carre May 13, 2026
01d41bb
Merge branch 'main' into 2038_basic_conversions
Matt-Carre May 13, 2026
9868b82
allows for boolean input to be rejected
Matt-Carre May 13, 2026
3d4e637
adds strictfloat validation
Matt-Carre May 13, 2026
1383712
Merge branch 'main' into 2038_basic_conversions
Matt-Carre May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
93 changes: 93 additions & 0 deletions src/dodal/common/general_maths/arithmetic_conversions.py
Comment thread
Matt-Carre marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
def convert_percentage_to_factor(pc: float):
Comment thread
Matt-Carre marked this conversation as resolved.
Outdated
"""Takes a percentage value and converts it the corresponding multiplication factor.

Args:
pc (float): a percentage
Returns:
pc (float): a factor
"""
return pc * 1e-2


def convert_factor_to_percentage(f: float):
"""Takes a multiplication factor and converts it to the corresponding percentage.

Args:
f (float): a factor

Returns:
f (float): a percentage
"""
return f * 1e2


def convert_microns_to_cm(t_um: float):
"""Takes the numerical part of a distance in microns and converts this to cm.

Args:
t_um (float): a distance in microns

Returns:
t_um (float): a distance in cm
"""
return t_um * 1e-4


def convert_microns_to_mm(v_um: float):
"""Takes the numerical part of a distance in microns and converts this to mm.

Args:
v_um (float): a distance in microns

Returns:
v_um (float): a distance in mm
"""
return v_um * 1e-3


def convert_mm_to_microns(w_mm: float):
"""Takes the numerical part of a distance in mm and converts this to microns.

Args:
w_mm (float): a distance in mm

Returns:
w_um (float): a distance in microns
"""
return w_mm * 1e3


def convert_mm_to_cm(x_mm: float):
"""Takes the numerical part of a distance in mm and converts this to cm.

Args:
x_mm (float): a distance in mm

Returns:
w_um (float): a distance in cm
"""
return x_mm * 1e-1


def convert_cm_to_mm(y_cm: float):
"""Takes the numerical part of a distance in cm and converts this to mm.

Args:
y_cm (float): a distance in cm

Returns:
w_um (float): a distance in mm
"""
return y_cm * 1e1


def convert_ev_to_kev(energy_ev: float):
"""Takes the numerical part of an x-ray energy in electron volts and converts this
to keV.

Args: energy_ev (float): a value of electron volts

Returns:
energy_ev (float): a value of kilo-electron volts
"""
return energy_ev * 1e-3
33 changes: 33 additions & 0 deletions src/dodal/common/general_maths/check_bounds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
def subordinate_range_test(lower_bound: float, upper_bound: float, x: float):
outside_range = x < lower_bound or x > upper_bound
return not outside_range


def is_within_range(lower_bound: float, upper_bound: float, tested_value: float):
"""Checks if a single value falls between two bounds, a lower (first) and an upper
(second).

Args:
lower_bound (float): the smaller of the two values
upper_bound (float): the greater of the two values
tested_value (float): the value to be tested

Returns:
outside_range (bool): True if the tested value falls between, or False if not
"""
# TODO check if we need to use pydantic for this.
if upper_bound < lower_bound:
Comment thread
Matt-Carre marked this conversation as resolved.
Outdated
value_error_message_template = (
"Range upper bound %.4f < lower bound %.4f is an invalid condition"
)
value_error_message = value_error_message_template % (
upper_bound,
lower_bound,
)
raise ValueError(value_error_message)
else:
return subordinate_range_test(
lower_bound,
upper_bound,
tested_value,
)
Empty file.
98 changes: 98 additions & 0 deletions tests/common/general_maths/test_check_bounds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import math

import pytest

from dodal.common.general_maths.check_bounds import is_within_range


# Happy Path
@pytest.mark.parametrize(
"lower_bound,upper_bound,tested_value,result",
[
(1, 3, 2, True),
(-3, -1, -2, True),
(-1, 1, 0, True),
(-1, -0.1, -0.5, True),
(0.1, 1, 0.5, True),
],
)
def test_is_within_range(
lower_bound: float, upper_bound: float, tested_value: float, result: bool
):
assert is_within_range(lower_bound, upper_bound, tested_value) == result


@pytest.mark.parametrize(
"lower_bound,upper_bound,tested_value,result",
[
(1, 2, 3, False),
(-3, -1, -4, False),
(1, 2, 0, False),
(-1, -0.1, -10.0, False),
(0.1, 1, 1.5, False),
],
)
def test_is_outside_range(
lower_bound: float, upper_bound: float, tested_value: float, result: bool
):
assert is_within_range(lower_bound, upper_bound, tested_value) == result


# Inauspicious Path
def test_has_misordered_inputs():
with pytest.raises(ValueError):
is_within_range(4, -4, 1)


@pytest.mark.parametrize(
"bad_input",
[
(
"a",
None,
math.sin,
object(),
),
],
)
def test_is_within_range_raises_error_with_bad_tested_value(
bad_input,
):
with pytest.raises(TypeError):
is_within_range(-4, 4, bad_input)


@pytest.mark.parametrize(
"bad_input",
[
(
"a",
None,
math.sin,
object(),
),
],
)
def test_is_within_range_raises_error_with_bad_upper_bound(
bad_input,
):
with pytest.raises(TypeError):
is_within_range(-4, bad_input, 4)


@pytest.mark.parametrize(
"bad_input",
[
(
"a",
None,
math.sin,
object(),
),
],
)
def test_is_within_range_raises_error_with_bad_lower_bound(
bad_input,
):
with pytest.raises(TypeError):
is_within_range(bad_input, 4, 0)
38 changes: 38 additions & 0 deletions tests/common/general_maths/test_conversion_cm_and_mm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import math

import pytest

from dodal.common.general_maths.arithmetic_conversions import (
convert_cm_to_mm,
convert_mm_to_cm,
)


# expected success tests (the 'Happy Path'): All numbers here are arbitrary
@pytest.mark.parametrize("input,result", [(1.0, 0.1), (100.0, 10.0)])
def test_conversion_from_millimetres_to_centimetres(input, result):
assert convert_mm_to_cm(input) == pytest.approx(result)


@pytest.mark.parametrize("input,result", [(1.0, 10), (0.1, 1.0)])
def test_conversion_from_centimetres_to_millimetres(input, result):
assert convert_cm_to_mm(input) == pytest.approx(result)


# The inauspicuous path
@pytest.mark.parametrize(
"bad_input",
["a", [], None, math.sin, object()],
)
def test_convert_mm_to_cm_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_mm_to_cm(bad_input)


@pytest.mark.parametrize(
"bad_input",
["a", [], None, math.sin, object()],
)
def test_convert_cm_to_mm_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_cm_to_mm(bad_input)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import math

import pytest

from dodal.common.general_maths.arithmetic_conversions import (
convert_factor_to_percentage,
convert_percentage_to_factor,
)

# expected success tests (the 'Happy Path'): All numbers here are arbitrary


@pytest.mark.parametrize("input,result", [(0.01, 1.0), (1.0, 100.0)])
def test_conversion_to_percentage_from_factor(input, result):
assert convert_factor_to_percentage(input) == pytest.approx(result)


@pytest.mark.parametrize("input,result", [(1.0, 0.01), (100, 1.0)])
def test_conversion_to_factor_from_percentage(input, result):
assert convert_percentage_to_factor(input) == pytest.approx(result)


# The inauspicuous path
@pytest.mark.parametrize(
"bad_input",
["a", [], None, math.sin, object()],
)
def test_convert_factor_to_percentage_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_factor_to_percentage(bad_input)


@pytest.mark.parametrize(
"bad_input",
["a", [], None, math.sin, object()],
)
def test_convert_percentage_to_factor_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_percentage_to_factor(bad_input)
38 changes: 38 additions & 0 deletions tests/common/general_maths/test_conversion_mm_and_microns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import math

import pytest

from dodal.common.general_maths.arithmetic_conversions import (
convert_microns_to_mm,
convert_mm_to_microns,
)


# expected success tests (the 'Happy Path'): All numbers here are arbitrary
@pytest.mark.parametrize("input,result", [(1000.0, 1.0), (10000.0, 10.0)])
def test_conversion_from_microns_to_millimeters(input, result):
assert convert_microns_to_mm(input) == pytest.approx(result)


@pytest.mark.parametrize("input,result", [(1.0, 1000.0), (10, 10000.0)])
def test_conversion_from_millimeters_to_microns(input, result):
assert convert_mm_to_microns(input) == pytest.approx(result)


# The inauspicuous path
@pytest.mark.parametrize(
"bad_input",
["a", [], None, math.sin, object()],
)
def test_convert_microns_to_mm_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_microns_to_mm(bad_input)


@pytest.mark.parametrize(
"bad_input",
["a", [], None, math.sin, object()],
)
def test_convert_mm_to_microns_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_mm_to_microns(bad_input)
24 changes: 24 additions & 0 deletions tests/common/general_maths/test_convert_ev_to_kev.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import math

import pytest

from dodal.common.general_maths.arithmetic_conversions import (
convert_ev_to_kev,
)

# expected success tests (the 'Happy Path'): All numbers here are arbitrary


@pytest.mark.parametrize("input,result", [(1000, 1.0), (100, 0.1)])
def test_conversion_from_electronvolts_to_kiloelectronvolts(input, result):
assert convert_ev_to_kev(input) == pytest.approx(result)


# The inauspicuous path
@pytest.mark.parametrize(
"bad_input",
["a", [], None, math.sin, object()],
)
def test_convert_ev_to_kev_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_ev_to_kev(bad_input)
24 changes: 24 additions & 0 deletions tests/common/general_maths/test_convert_microns_to_cm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import math

import pytest

from dodal.common.general_maths.arithmetic_conversions import (
convert_microns_to_cm,
)

# expected success tests (the 'Happy Path'): All numbers here are arbitrary


@pytest.mark.parametrize("input,result", [(10000.0, 1.0), (1000, 0.1)])
def test_conversion_from_microns_to_centimetres(input, result):
assert convert_microns_to_cm(input) == pytest.approx(result)


# The inauspicuous path
@pytest.mark.parametrize(
"bad_input",
["a", [], None, math.sin, object()],
)
def test_convert_microns_to_cm_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_microns_to_cm(bad_input)
Loading