feat: add EnergyOptimizer class with transition step calculation and tests

This commit is contained in:
2025-03-23 18:38:12 +01:00
parent a920440594
commit dbda93ee12
2 changed files with 121 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
from __future__ import annotations
from typing import TYPE_CHECKING, List, Tuple
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import DeviceManagerBase as DeviceManager
class EnergyOptimizer:
def __init__(self, device_manager: DeviceManager):
self.device_manager = device_manager
self.strips = {
"Si": {"energy_range": [5, 10]},
"Rh": {"energy_range": [8, 23]},
"Pt": {"energy_range": [20, 40]},
}
self.overlap_energyies = {"Si": {"Rh": 9}, "Rh": {"Si": 9, "Pt": 22}, "Pt": {"Rh": 22}}
@staticmethod
def get_transition_steps(start_energy: float, target_energy: float) -> List[Tuple[float, str]]:
"""
Get the required steps to transition from one energy to another.
Args:
start_energy: The starting energy in keV.
target_energy: The target energy in keV.
Returns:
A list of tuples containing the energy and the strip name for each step.
"""
strips = {"Si": (5, 10), "Rh": (8, 23), "Pt": (20, 40)}
overlap_energyies = {"Si": {"Rh": 9}, "Rh": {"Si": 9, "Pt": 22}, "Pt": {"Rh": 22}}
def get_strip(energy: float) -> str:
for strip, (low, high) in strips.items():
if low <= energy <= high:
return strip
return ""
def find_overlap(from_strip: str, to_strip: str) -> float:
return overlap_energyies[from_strip][to_strip]
path = []
if get_strip(start_energy) == "":
raise ValueError("Start energy is out of range for available strips")
if not any(low <= target_energy <= high for low, high in strips.values()):
raise ValueError("End energy is out of range for available strips")
current_energy = start_energy
# TODO: this should be replaced with a readout from the PV
current_strip = get_strip(current_energy)
# if the target energy is covered by the current strip, return the path
if strips[current_strip][0] <= target_energy <= strips[current_strip][1]:
return [(target_energy, current_strip)]
target_strip = get_strip(target_energy)
available_strips = list(strips.keys())
current_index = available_strips.index(current_strip)
target_index = available_strips.index(target_strip)
step = 1 if target_index > current_index else -1
for i in range(current_index, target_index, step):
next_strip = available_strips[i + step]
overlap_energy = find_overlap(available_strips[i], next_strip)
path.append((overlap_energy, next_strip))
current_strip = next_strip
path.append((target_energy, target_strip))
return path
if __name__ == "__main__": # pragma: no cover
from unittest.mock import MagicMock
device_manager = MagicMock()
optimizer = EnergyOptimizer(device_manager)
steps = optimizer.get_transition_steps(30, 20)
print(steps)

View File

@@ -0,0 +1,38 @@
from unittest import mock
import pytest
from addams_bec.bec_ipython_client.plugins.energy_optimizer.addams_energy_optimizer import (
EnergyOptimizer,
)
@pytest.fixture
def optimizer():
dm = mock.MagicMock()
yield EnergyOptimizer(dm)
@pytest.mark.parametrize(
"start_energy, target_energy, expected",
[
(5, 10, [(10, "Si")]),
(5, 20, [(9, "Rh"), (20, "Rh")]),
(5, 40, [(9, "Rh"), (22, "Pt"), (40, "Pt")]),
(5, 5, [(5, "Si")]),
(5, 4, ValueError),
(5, 41, ValueError),
(2, 8, ValueError),
(18, 40, [(22, "Pt"), (40, "Pt")]),
(18, 5, [(9, "Si"), (5, "Si")]),
(18, 10, [(10, "Rh")]),
(18, 20, [(20, "Rh")]),
(25, 7, [(22, "Rh"), (9, "Si"), (7, "Si")]),
],
)
def test_get_transition_steps(optimizer, start_energy, target_energy, expected):
if expected == ValueError:
with pytest.raises(ValueError):
optimizer.get_transition_steps(start_energy, target_energy)
else:
assert optimizer.get_transition_steps(start_energy, target_energy) == expected