Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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.
43 changes: 43 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,43 @@
def calculate(num: float, multiplier: float):
Comment thread
Matt-Carre marked this conversation as resolved.
Outdated
return num * multiplier


def convert_percentage_to_factor(pc: float):
# Takes a percentage value and converts it the corresponding multiplication factor
Comment thread
Matt-Carre marked this conversation as resolved.
Outdated
return calculate(pc, 1e-2)


def convert_factor_to_percentage(f: float):
# Takes a multiplication factor and converts it to the corresponding percentage
return calculate(f, 1e2)


def convert_microns_to_cm(t_um: float):
# Takes the numerical part of a distance in microns and converts this to cm
return calculate(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
return calculate(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
return calculate(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
return calculate(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
return calculate(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.
return calculate(energy_ev, 1e-3)
22 changes: 22 additions & 0 deletions src/dodal/common/general_maths/check_bounds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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):

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.
129 changes: 129 additions & 0 deletions tests/common/general_maths/test_arithmetic_conversions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import math

import pytest

from dodal.common.general_maths.arithmetic_conversions import (
convert_cm_to_mm,
convert_ev_to_kev,
convert_factor_to_percentage,
convert_microns_to_cm,
convert_microns_to_mm,
convert_mm_to_cm,
convert_mm_to_microns,
convert_percentage_to_factor,
)

# expected success tests (the 'Happy Path'):


@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)


@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)


@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)


@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)


@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_cm_to_mm_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_cm_to_mm(bad_input)


@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)


@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_microns_to_cm_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_microns_to_cm(bad_input)


@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_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_mm_to_microns_raises_error_with_bad_input(bad_input):
with pytest.raises(TypeError):
convert_mm_to_microns(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)
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)
Loading