Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
6 changes: 3 additions & 3 deletions src/dodal/beamlines/i03.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def daq_configuration_path() -> str:

@devices.factory()
def aperture_scatterguard() -> ApertureScatterguard:
params = get_beamline_parameters()
params = get_beamline_parameters(BL)
return ApertureScatterguard(
aperture_prefix=f"{PREFIX.beamline_prefix}-MO-MAPT-01:",
scatterguard_prefix=f"{PREFIX.beamline_prefix}-MO-SCAT-01:",
Expand All @@ -116,7 +116,7 @@ def attenuator() -> BinaryFilterAttenuator:
def beamstop() -> Beamstop:
return Beamstop(
prefix=f"{PREFIX.beamline_prefix}-MO-BS-01:",
beamline_parameters=get_beamline_parameters(),
beamline_parameters=get_beamline_parameters(BL),
)


Expand Down Expand Up @@ -346,7 +346,7 @@ def scintillator(aperture_scatterguard: ApertureScatterguard) -> Scintillator:
return Scintillator(
f"{PREFIX.beamline_prefix}-MO-SCIN-01:",
Reference(aperture_scatterguard),
get_beamline_parameters(),
get_beamline_parameters(BL),
)


Expand Down
6 changes: 3 additions & 3 deletions src/dodal/beamlines/i04.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def ipin() -> IPin:
def beamstop() -> Beamstop:
return Beamstop(
f"{PREFIX.beamline_prefix}-MO-BS-01:",
beamline_parameters=get_beamline_parameters(),
beamline_parameters=get_beamline_parameters(BL),
)


Expand Down Expand Up @@ -148,7 +148,7 @@ def backlight() -> Backlight:

@devices.factory()
def aperture_scatterguard() -> ApertureScatterguard:
params = get_beamline_parameters()
params = get_beamline_parameters(BL)
return ApertureScatterguard(
aperture_prefix=f"{PREFIX.beamline_prefix}-MO-MAPT-01:",
scatterguard_prefix=f"{PREFIX.beamline_prefix}-MO-SCAT-01:",
Expand Down Expand Up @@ -281,7 +281,7 @@ def scintillator(aperture_scatterguard: ApertureScatterguard) -> Scintillator:
return Scintillator(
f"{PREFIX.beamline_prefix}-MO-SCIN-01:",
Reference(aperture_scatterguard),
get_beamline_parameters(),
get_beamline_parameters(BL),
)


Expand Down
79 changes: 15 additions & 64 deletions src/dodal/common/beamlines/beamline_parameters.py
Original file line number Diff line number Diff line change
@@ -1,75 +1,26 @@
import ast
from typing import Any, cast
from typing import Any

from dodal.log import LOGGER
from dodal.utils import get_beamline_name

BEAMLINE_PARAMETER_KEYWORDS = ["FB", "FULL", "deadtime"]
from dodal.common.beamlines.config_client import get_config_client

BEAMLINE_PARAMETER_PATHS = {
"i03": "/dls_sw/i03/software/daq_configuration/domain/beamlineParameters",
"i04": "/dls_sw/i04/software/daq_configuration/domain/beamlineParameters",
}


class GDABeamlineParameters:
params: dict[str, Any]

def __init__(self, params: dict[str, Any]):
self.params = params

def __repr__(self) -> str:
return repr(self.params)

def __getitem__(self, item: str):
return self.params[item]

@classmethod
def from_lines(cls, file_name: str, config_lines: list[str]):
config_lines_nocomments = [line.split("#", 1)[0] for line in config_lines]
config_lines_sep_key_and_value = [
# XXX removes all whitespace instead of just trim
line.translate(str.maketrans("", "", " \n\t\r")).split("=")
for line in config_lines_nocomments
]
config_pairs: list[tuple[str, Any]] = [
cast(tuple[str, Any], param)
for param in config_lines_sep_key_and_value
if len(param) == 2
]
for i, (param, value) in enumerate(config_pairs):
try:
# BEAMLINE_PARAMETER_KEYWORDS effectively raw string but whitespace removed
if value not in BEAMLINE_PARAMETER_KEYWORDS:
config_pairs[i] = (
param,
cls.parse_value(value),
)
except Exception as e:
LOGGER.warning(f"Unable to parse {file_name} line {i}: {e}")

return cls(params=dict(config_pairs))

@classmethod
def from_file(cls, path: str):
with open(path) as f:
config_lines = f.readlines()
return cls.from_lines(path, config_lines)

@classmethod
def parse_value(cls, value: str):
return ast.literal_eval(value.replace("Yes", "True").replace("No", "False"))
def get_beamline_parameters(beamline: str) -> dict[str, Any]:
"""Loads the beamline parameters for a specified beamline from the config server.

Args:
beamline (str): The beamline for which beamline parameters will be retrieved.

def get_beamline_parameters(beamline_param_path: str | None = None):
"""Loads the beamline parameters from the specified path, or according to the
environment variable if none is given.
Returns:
dict[str, Any]: Dict of beamline parameters.
"""
if not beamline_param_path:
beamline_name = get_beamline_name("i03")
beamline_param_path = BEAMLINE_PARAMETER_PATHS.get(beamline_name)
if beamline_param_path is None:
raise KeyError(
"No beamline parameter path found, maybe 'BEAMLINE' environment variable is not set!"
)
return GDABeamlineParameters.from_file(beamline_param_path)
beamline_param_path = BEAMLINE_PARAMETER_PATHS.get(beamline)
if beamline_param_path is None:
raise KeyError(
"No beamline parameter path found, maybe 'BEAMLINE' environment variable is not set!"
)
config_client = get_config_client(beamline)
return config_client.get_file_contents(beamline_param_path, dict)
16 changes: 16 additions & 0 deletions src/dodal/common/beamlines/config_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from functools import cache

from daq_config_server.client import ConfigServer

BEAMLINE_CONFIG_SERVER_ENDPOINTS = {
"i03": "https://i03-daq-config.diamond.ac.uk",
"i04": "https://daq-config.diamond.ac.uk",
}


@cache
def get_config_client(beamline: str) -> ConfigServer:
url = BEAMLINE_CONFIG_SERVER_ENDPOINTS.get(
beamline, "https://daq-config.diamond.ac.uk"
)
return ConfigServer(url=url)
8 changes: 4 additions & 4 deletions src/dodal/devices/aperturescatterguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
from math import inf
from typing import Any

from bluesky.protocols import Preparable
from ophyd_async.core import (
Expand All @@ -14,7 +15,6 @@
)
from pydantic import BaseModel, Field

from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
from dodal.devices.aperture import Aperture
from dodal.devices.motors import XYStage

Expand Down Expand Up @@ -65,7 +65,7 @@ def values(self) -> tuple[float, float, float, float, float]:

@staticmethod
def tolerances_from_gda_params(
params: GDABeamlineParameters,
params: dict[str, Any],
) -> AperturePosition:
return AperturePosition(
aperture_x=params["miniap_x_tolerance"],
Expand All @@ -79,7 +79,7 @@ def tolerances_from_gda_params(
def from_gda_params(
name: _GDAParamApertureValue,
diameter: float,
params: GDABeamlineParameters,
params: dict[str, Any],
) -> AperturePosition:
return AperturePosition(
aperture_x=params[f"miniap_x_{name.value}"],
Expand Down Expand Up @@ -109,7 +109,7 @@ def __str__(self):


def load_positions_from_beamline_parameters(
params: GDABeamlineParameters,
params: dict[str, Any],
) -> dict[ApertureValue, AperturePosition]:
return {
ApertureValue.OUT_OF_BEAM: AperturePosition.from_gda_params(
Expand Down
7 changes: 4 additions & 3 deletions src/dodal/devices/beamlines/i03/undulator_dcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dodal.devices.beamlines.i03.dcm import DCM
from dodal.devices.undulator import UndulatorInKeV
from dodal.log import LOGGER
from dodal.utils import get_beamline_name

ENERGY_TIMEOUT_S: float = 30.0

Expand Down Expand Up @@ -47,9 +48,9 @@ def __init__(
)
# I03 configures the DCM Perp as a side effect of applying this fixed value to the DCM Offset after an energy change
# Nb this parameter is misleadingly named to confuse you
self.dcm_fixed_offset_mm = get_beamline_parameters(
daq_configuration_path + "/domain/beamlineParameters"
)["DCM_Perp_Offset_FIXED"]
self.dcm_fixed_offset_mm = get_beamline_parameters(get_beamline_name())[
"DCM_Perp_Offset_FIXED"
]

super().__init__(name)

Expand Down
5 changes: 2 additions & 3 deletions src/dodal/devices/mx_phase1/beamstop.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
from math import isclose
from typing import Any

from ophyd_async.core import (
StandardReadable,
Expand All @@ -8,8 +9,6 @@
)
from ophyd_async.epics.motor import Motor

from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters

_BEAMSTOP_OUT_DELTA_Y_MM = -2


Expand Down Expand Up @@ -46,7 +45,7 @@ class Beamstop(StandardReadable):
def __init__(
self,
prefix: str,
beamline_parameters: GDABeamlineParameters,
beamline_parameters: dict[str, Any],
name: str = "",
):
with self.add_children_as_readables():
Expand Down
4 changes: 2 additions & 2 deletions src/dodal/devices/scintillator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from math import isclose
from typing import Any

from ophyd_async.core import Reference, StandardReadable, StrictEnum, derived_signal_rw
from ophyd_async.epics.motor import Motor

from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue


Expand All @@ -29,7 +29,7 @@ def __init__(
self,
prefix: str,
aperture_scatterguard: Reference[ApertureScatterguard],
beamline_parameters: GDABeamlineParameters,
beamline_parameters: dict[str, Any],
name: str = "",
):
with self.add_children_as_readables():
Expand Down
5 changes: 3 additions & 2 deletions src/dodal/plan_stubs/check_topup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
)
from dodal.devices.synchrotron import Synchrotron, SynchrotronMode
from dodal.log import LOGGER
from dodal.utils import get_beamline_name

ALLOWED_MODES = [SynchrotronMode.USER, SynchrotronMode.SPECIAL]
DECAY_MODE_COUNTDOWN = -1 # Value of the start_countdown PV when in decay mode
Expand Down Expand Up @@ -133,5 +134,5 @@ def check_topup_and_wait_if_necessary(


def _load_topup_configuration_from_properties_file() -> dict[str, Any]:
params = get_beamline_parameters()
return params.params
params = get_beamline_parameters(get_beamline_name("i03"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like this practice that we have in the code of inserting arbitrary default beamline names into random bits of code. By removing the default from get_beamline_parameters() this has just spread it around more.

I think it would be better if we never assumed it anywhere in production code and instead took it from the BEAMLINE environment variable in a single function which we can patch out for unit tests.

Copy link
Contributor Author

@jacob720 jacob720 Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically remove the 'default' argument from get_beamline_name()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had a quick go at doing this and it's looking like a big change, and I'm not 100% sure of the consequences, so I think we should pull it out into a new issue. For now, I'll make the default argument optional so that we don't have to arbitrarily decide on defaults.

return params
38 changes: 38 additions & 0 deletions src/dodal/testing/fixtures/config_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import json
from pathlib import Path
from typing import TypeVar
from unittest.mock import patch

import pytest
from daq_config_server.models import ConfigModel

T = TypeVar("T", str, dict, ConfigModel)


def fake_config_server_get_file_contents(
filepath: str | Path,
desired_return_type: type[T] = str,
reset_cached_result: bool = True,
) -> T:
filepath = Path(filepath)
# Minimal logic required for unit tests
with filepath.open("r") as f:
contents = f.read()
if desired_return_type is str:
return contents # type: ignore
elif desired_return_type is dict:
return json.loads(contents)
elif issubclass(desired_return_type, ConfigModel):
return desired_return_type.model_validate(json.loads(contents))
raise ValueError("Invalid return type requested")


@pytest.fixture(autouse=True)
def mock_config_server():
# Don't actually talk to central service during unit tests, and reset caches between test

with patch(
"daq_config_server.client.ConfigServer.get_file_contents",
side_effect=fake_config_server_get_file_contents,
):
yield
7 changes: 5 additions & 2 deletions src/dodal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@
AnyDeviceFactory: TypeAlias = V1DeviceFactory | V2DeviceFactory


def get_beamline_name(default: str) -> str:
return environ.get("BEAMLINE") or default
def get_beamline_name(default: str | None = None) -> str:
beamline_name = environ.get("BEAMLINE") or default
if beamline_name is None:
raise ValueError("Set BEAMLINE environment variable or provide default.")
return beamline_name


def is_test_mode() -> bool:
Expand Down
Loading