Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
babfea4
Register dcg for atlases
stephen-riggs Nov 20, 2025
a9249da
Always send atlas with tomo metadata dcg registration
stephen-riggs Nov 21, 2025
6e08588
Always yse tomo metadata function for tomo dcgs
stephen-riggs Nov 21, 2025
663361c
Common function for dcg registration
stephen-riggs Nov 21, 2025
6fe40c2
Always use the dcg function in the client
stephen-riggs Nov 21, 2025
6bfade7
Workflow to change experiment type id
stephen-riggs Nov 21, 2025
2d13a05
Test for atlas context
stephen-riggs Nov 21, 2025
2b9f478
Add hook for epu session
stephen-riggs Nov 21, 2025
f258c97
Catch failed hook, and add some tests
stephen-riggs Nov 25, 2025
a366480
Fix test which broke on moving file
stephen-riggs Nov 25, 2025
1356eba
More tests
stephen-riggs Nov 25, 2025
8f21e09
Try and test ispyb db update
stephen-riggs Nov 25, 2025
3b02ae6
Sort out ids
stephen-riggs Nov 25, 2025
1de4a89
Try more comprehensive tomo metadata test
stephen-riggs Nov 25, 2025
b15a463
Try fixing tests
stephen-riggs Nov 25, 2025
3a76ca5
test transport init
stephen-riggs Nov 25, 2025
46012ab
Find ou twha the calls are
stephen-riggs Nov 25, 2025
4d8d0d7
Keep trying
stephen-riggs Nov 25, 2025
4f99666
Don't bother iwth the connect
stephen-riggs Nov 25, 2025
dcb94e5
More layers
stephen-riggs Nov 25, 2025
f33c945
Print object to see what it is
stephen-riggs Nov 26, 2025
1a496f8
Different way of patching
stephen-riggs Nov 26, 2025
c654ddd
Mock without the call method
stephen-riggs Nov 26, 2025
dc35e9f
Mock as return value
stephen-riggs Nov 26, 2025
2da2b4d
DCGs are based on atlas jpg name
stephen-riggs Nov 27, 2025
b6ba908
Second atlases are possible, and need to avoid experiment type flip-f…
stephen-riggs Nov 27, 2025
1533125
Merge branch 'atlas-dcg' into match-atlas-dcgs
stephen-riggs Nov 27, 2025
01b1283
Add some tests for the api dcg insert
stephen-riggs Nov 27, 2025
f8d7790
Mokced the wrong thing
stephen-riggs Nov 27, 2025
8d957af
Fix type of the dcgparams
stephen-riggs Nov 27, 2025
04bdc27
Minor messups in the tests
stephen-riggs Nov 27, 2025
9df629d
mocked wrong way around
stephen-riggs Nov 27, 2025
97f9ff0
Yet another tuple layer
stephen-riggs Nov 27, 2025
608f9ad
Wrong tag
stephen-riggs Nov 27, 2025
796fb71
Rename hook
stephen-riggs Dec 1, 2025
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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ GitHub = "https://github.com/DiamondLightSource/python-murfey"
"clem.register_preprocessing_result" = "murfey.workflows.clem.register_preprocessing_results:run"
"data_collection" = "murfey.workflows.register_data_collection:run"
"data_collection_group" = "murfey.workflows.register_data_collection_group:run"
"experiment_type_update" = "murfey.workflows.register_experiment_type_update:run"
"pato" = "murfey.workflows.notifications:notification_setup"
"picked_particles" = "murfey.workflows.spa.picking:particles_picked"
"picked_tomogram" = "murfey.workflows.tomo.picking:picked_tomogram"
Expand Down
150 changes: 145 additions & 5 deletions src/murfey/client/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,156 @@
import logging
from importlib.metadata import entry_points
from pathlib import Path
from typing import Any, Dict, List, NamedTuple
from typing import Any, List, NamedTuple

from murfey.client.instance_environment import MurfeyInstanceEnvironment
import xmltodict

from murfey.client.instance_environment import MurfeyInstanceEnvironment, SampleInfo
from murfey.util.client import capture_post, get_machine_config_client

logger = logging.getLogger("murfey.client.context")


class FutureRequest(NamedTuple):
url: str
message: Dict[str, Any]
def _atlas_destination(
environment: MurfeyInstanceEnvironment, source: Path, token: str
) -> Path:
machine_config = get_machine_config_client(
str(environment.url.geturl()),
token,
instrument_name=environment.instrument_name,
demo=environment.demo,
)
for i, destination_part in enumerate(
Path(environment.default_destinations[source]).parts
):
if destination_part == environment.visit:
return Path(machine_config.get("rsync_basepath", "")) / "/".join(
Path(environment.default_destinations[source]).parent.parts[: i + 1]
)
return (
Path(machine_config.get("rsync_basepath", ""))
/ Path(environment.default_destinations[source]).parent
/ environment.visit
)


def ensure_dcg_exists(
collection_type: str,
metadata_source: Path,
environment: MurfeyInstanceEnvironment,
token: str,
) -> str | None:
"""Create a data collection group"""
if collection_type == "tomo":
experiment_type_id = 36
session_file = metadata_source / "Session.dm"
elif collection_type == "spa":
experiment_type_id = 37
session_file = metadata_source / "EpuSession.dm"
for h in entry_points(group="murfey.hooks"):
try:
if h.name == "get_epu_session":
h.load()(session_file, environment=environment)
except Exception as e:
logger.warning(f"Get EPU session hook failed: {e}")
else:
logger.error(f"Unknown collection type {collection_type}")
return None

if not session_file.is_file():
logger.warning(f"Cannot find session file {str(session_file)}")
dcg_tag = (
str(metadata_source).replace(f"/{environment.visit}", "").replace("//", "/")
)
dcg_data = {
"experiment_type_id": experiment_type_id,
"tag": dcg_tag,
}
else:
with open(session_file, "r") as session_xml:
session_data = xmltodict.parse(session_xml.read())

if collection_type == "tomo":
windows_path = session_data["TomographySession"]["AtlasId"]
else:
windows_path = session_data["EpuSessionXml"]["Samples"]["_items"][
"SampleXml"
][0]["AtlasId"]["#text"]

logger.info(f"Windows path to atlas metadata found: {windows_path}")
if not windows_path:
logger.warning("No atlas metadata path found")
return None
visit_index = windows_path.split("\\").index(environment.visit)
partial_path = "/".join(windows_path.split("\\")[visit_index + 1 :])
logger.info("Partial Linux path successfully constructed from Windows path")

source_visit_dir = metadata_source.parent
logger.info(
f"Looking for atlas XML file in metadata directory {str((source_visit_dir / partial_path).parent)}"
)
atlas_xml_path = list(
(source_visit_dir / partial_path).parent.glob("Atlas_*.xml")
)[0]
logger.info(f"Atlas XML path {str(atlas_xml_path)} found")
with open(atlas_xml_path, "rb") as atlas_xml:
atlas_xml_data = xmltodict.parse(atlas_xml)
atlas_original_pixel_size = float(
atlas_xml_data["MicroscopeImage"]["SpatialScale"]["pixelSize"]["x"][
"numericValue"
]
)
# need to calculate the pixel size of the downscaled image
atlas_pixel_size = atlas_original_pixel_size * 7.8
logger.info(f"Atlas image pixel size determined to be {atlas_pixel_size}")

for p in partial_path.split("/"):
if p.startswith("Sample"):
sample = int(p.replace("Sample", ""))
break
else:
logger.warning(f"Sample could not be identified for {metadata_source}")
return None
environment.samples[metadata_source] = SampleInfo(
atlas=Path(partial_path), sample=sample
)

dcg_search_dir = (
str(metadata_source).replace(f"/{environment.visit}", "").replace("//", "/")
)
if collection_type == "tomo":
dcg_tag = dcg_search_dir
else:
dcg_images_dirs = sorted(
Path(dcg_search_dir).glob("Images-Disc*"),
key=lambda x: x.stat().st_ctime,
)
if not dcg_images_dirs:
logger.warning(f"Cannot find Images-Disc* in {dcg_search_dir}")
return None
dcg_tag = str(dcg_images_dirs[-1])

dcg_data = {
"experiment_type_id": experiment_type_id,
"tag": dcg_tag,
"atlas": str(
_atlas_destination(environment, metadata_source, token)
/ environment.samples[metadata_source].atlas.parent
/ atlas_xml_path.with_suffix(".jpg").name
).replace("//", "/"),
"sample": environment.samples[metadata_source].sample,
"atlas_pixel_size": atlas_pixel_size,
}
capture_post(
base_url=str(environment.url.geturl()),
router_name="workflow.router",
function_name="register_dc_group",
token=token,
visit_name=environment.visit,
session_id=environment.murfey_session,
data=dcg_data,
)
return dcg_tag


class ProcessingParameter(NamedTuple):
Expand Down
5 changes: 2 additions & 3 deletions src/murfey/client/contexts/atlas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
from pathlib import Path
from typing import Optional

from murfey.client.context import Context
from murfey.client.context import Context, _atlas_destination
from murfey.client.contexts.spa import _get_source
from murfey.client.contexts.spa_metadata import _atlas_destination
from murfey.client.instance_environment import MurfeyInstanceEnvironment
from murfey.util.client import capture_post

Expand Down Expand Up @@ -36,7 +35,7 @@ def post_transfer(
source = _get_source(transferred_file, environment)
if source:
transferred_atlas_name = _atlas_destination(
environment, source, transferred_file, self._token
environment, source, self._token
) / transferred_file.relative_to(source.parent)
capture_post(
base_url=str(environment.url.geturl()),
Expand Down
143 changes: 16 additions & 127 deletions src/murfey/client/contexts/spa_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

import xmltodict

from murfey.client.context import Context
from murfey.client.context import Context, ensure_dcg_exists
from murfey.client.contexts.spa import _file_transferred_to, _get_source
from murfey.client.instance_environment import MurfeyInstanceEnvironment, SampleInfo
from murfey.util.client import capture_post, get_machine_config_client
from murfey.client.instance_environment import MurfeyInstanceEnvironment
from murfey.util.client import capture_post
from murfey.util.spa_metadata import (
FoilHoleInfo,
get_grid_square_atlas_positions,
Expand Down Expand Up @@ -69,29 +69,6 @@ def _foil_hole_positions(xml_path: Path, grid_square: int) -> Dict[str, FoilHole
return foil_holes


def _atlas_destination(
environment: MurfeyInstanceEnvironment, source: Path, file_path: Path, token: str
) -> Path:
machine_config = get_machine_config_client(
str(environment.url.geturl()),
token,
instrument_name=environment.instrument_name,
demo=environment.demo,
)
for i, destination_part in enumerate(
Path(environment.default_destinations[source]).parts
):
if destination_part == environment.visit:
return Path(machine_config.get("rsync_basepath", "")) / "/".join(
Path(environment.default_destinations[source]).parent.parts[: i + 1]
)
return (
Path(machine_config.get("rsync_basepath", ""))
/ Path(environment.default_destinations[source]).parent
/ environment.visit
)


class SPAMetadataContext(Context):
def __init__(self, acquisition_software: str, basepath: Path, token: str):
super().__init__("SPA_metadata", acquisition_software, token)
Expand Down Expand Up @@ -124,82 +101,19 @@ def post_transfer(
source = _get_source(transferred_file, environment)
if not source:
logger.warning(
f"Source could not be indentified for {str(transferred_file)}"
f"Source could not be identified for {str(transferred_file)}"
)
return

source_visit_dir = source.parent

logger.info(
f"Looking for atlas XML file in metadata directory {str((source_visit_dir / partial_path).parent)}"
)
atlas_xml_path = list(
(source_visit_dir / partial_path).parent.glob("Atlas_*.xml")
)[0]
logger.info(f"Atlas XML path {str(atlas_xml_path)} found")
with open(atlas_xml_path, "rb") as atlas_xml:
atlas_xml_data = xmltodict.parse(atlas_xml)
atlas_original_pixel_size = float(
atlas_xml_data["MicroscopeImage"]["SpatialScale"]["pixelSize"]["x"][
"numericValue"
]
)

# need to calculate the pixel size of the downscaled image
atlas_pixel_size = atlas_original_pixel_size * 7.8
logger.info(f"Atlas image pixel size determined to be {atlas_pixel_size}")

for p in partial_path.split("/"):
if p.startswith("Sample"):
sample = int(p.replace("Sample", ""))
break
else:
logger.warning(f"Sample could not be identified for {transferred_file}")
return
if source:
environment.samples[source] = SampleInfo(
atlas=Path(partial_path), sample=sample
)
dcg_search_dir = "/".join(
p for p in transferred_file.parent.parts if p != environment.visit
)
dcg_search_dir = (
dcg_search_dir[1:]
if dcg_search_dir.startswith("//")
else dcg_search_dir
)
dcg_images_dirs = sorted(
Path(dcg_search_dir).glob("Images-Disc*"),
key=lambda x: x.stat().st_ctime,
)
if not dcg_images_dirs:
logger.warning(f"Cannot find Images-Disc* in {dcg_search_dir}")
return
dcg_tag = str(dcg_images_dirs[-1])
dcg_data = {
"experiment_type_id": 37, # Single particle
"tag": dcg_tag,
"atlas": str(
_atlas_destination(
environment, source, transferred_file, self._token
)
/ environment.samples[source].atlas.parent
/ atlas_xml_path.with_suffix(".jpg").name
),
"sample": environment.samples[source].sample,
"atlas_pixel_size": atlas_pixel_size,
}
capture_post(
base_url=str(environment.url.geturl()),
router_name="workflow.router",
function_name="register_dc_group",
dcg_tag = ensure_dcg_exists(
collection_type="spa",
metadata_source=source,
environment=environment,
token=self._token,
visit_name=environment.visit,
session_id=environment.murfey_session,
data=dcg_data,
)
gs_pix_positions = get_grid_square_atlas_positions(
source_visit_dir / partial_path
source.parent / partial_path
)
for gs, pos_data in gs_pix_positions.items():
if pos_data:
Expand Down Expand Up @@ -228,46 +142,21 @@ def post_transfer(
and environment
):
# Make sure we have a data collection group before trying to register grid square
dcg_search_dir = "/".join(
p
for p in transferred_file.parent.parent.parts
if p != environment.visit
)
dcg_search_dir = (
dcg_search_dir[1:]
if dcg_search_dir.startswith("//")
else dcg_search_dir
)
dcg_images_dirs = sorted(
Path(dcg_search_dir).glob("Images-Disc*"),
key=lambda x: x.stat().st_ctime,
)
if not dcg_images_dirs:
logger.warning(f"Cannot find Images-Disc* in {dcg_search_dir}")
return
dcg_tag = str(dcg_images_dirs[-1])
dcg_data = {
"experiment_type_id": 37, # Single particle
"tag": dcg_tag,
}
capture_post(
base_url=str(environment.url.geturl()),
router_name="workflow.router",
function_name="register_dc_group",
source = _get_source(transferred_file, environment=environment)
if source is None:
return None
ensure_dcg_exists(
collection_type="spa",
metadata_source=source,
environment=environment,
token=self._token,
visit_name=environment.visit,
session_id=environment.murfey_session,
data=dcg_data,
)

gs_name = int(transferred_file.stem.split("_")[1])
logger.info(
f"Collecting foil hole positions for {str(transferred_file)} and grid square {gs_name}"
)
fh_positions = _foil_hole_positions(transferred_file, gs_name)
source = _get_source(transferred_file, environment=environment)
if source is None:
return None
visitless_source_search_dir = str(source).replace(
f"/{environment.visit}", ""
)
Expand Down
Loading
Loading