Skip to content

Commit fd1c146

Browse files
authored
Add interleave vds generation (#86)
1 parent d6971c2 commit fd1c146

6 files changed

Lines changed: 549 additions & 11 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies = [
1616
"numpy",
1717
"pillow",
1818
"typer",
19+
"h5py",
1920
] # Add project dependencies here, e.g. ["click", "numpy"]
2021
dynamic = ["version"]
2122
license.file = "LICENSE"
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastcs.attributes import AttrRW
1+
from fastcs.attributes import AttrR, AttrRW
22
from fastcs_odin.controllers.odin_data.frame_processor import (
33
FrameProcessorAdapterController,
44
)
@@ -7,3 +7,5 @@
77
class EigerFrameProcessorAdapterController(FrameProcessorAdapterController):
88
data_compression: AttrRW[str]
99
data_datatype: AttrRW[str]
10+
data_dims_0: AttrR[int] # y
11+
data_dims_1: AttrR[int] # x

src/fastcs_eiger/controllers/odin/eiger_odin_controller.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import asyncio
2+
from pathlib import Path
23

34
from fastcs.attributes import AttrRW
45
from fastcs.connections import IPConnectionSettings
5-
from fastcs.datatypes import Int
6+
from fastcs.datatypes import Bool, Int
67
from fastcs.methods import command
78

89
from fastcs_eiger.controllers.eiger_controller import COMMAND_GROUP, EigerController
10+
from fastcs_eiger.controllers.odin.generate_vds import create_interleave_vds
911
from fastcs_eiger.controllers.odin.odin_controller import OdinController
1012
from fastcs_eiger.eiger_parameter import EigerAPIVersion
1113

@@ -19,6 +21,7 @@ class EigerOdinController(EigerController):
1921
description="Timeout for start writing command",
2022
group=COMMAND_GROUP,
2123
)
24+
enable_vds_creation = AttrRW(Bool())
2225

2326
def __init__(
2427
self,
@@ -63,6 +66,10 @@ async def start_writing(self):
6366
self.OD.FP.data_datatype.put(f"uint{self.detector.bit_depth_image.get()}"),
6467
)
6568

69+
path = Path(self.OD.file_path.get())
70+
prefix = self.OD.file_prefix.get()
71+
frame_count = self.OD.FP.frames.get()
72+
6673
await self.OD.FP.start_writing()
6774

6875
try:
@@ -71,3 +78,18 @@ async def start_writing(self):
7178
)
7279
except TimeoutError as e:
7380
raise TimeoutError("File writers failed to start") from e
81+
82+
if self.enable_vds_creation.get():
83+
create_interleave_vds(
84+
path=path,
85+
prefix=prefix,
86+
datasets=["data", "data2", "data3"],
87+
frame_count=frame_count,
88+
frames_per_block=self.OD.block_size.get(),
89+
blocks_per_file=self.OD.FP.process_blocks_per_file.get(),
90+
frame_shape=(
91+
self.OD.FP.data_dims_1.get(),
92+
self.OD.FP.data_dims_0.get(),
93+
),
94+
dtype=self.OD.FP.data_datatype.get(),
95+
)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import math
2+
from dataclasses import dataclass
3+
from pathlib import Path
4+
5+
import h5py
6+
from fastcs.logging import logger
7+
8+
9+
@dataclass
10+
class FileFrames:
11+
frames: int
12+
start: int
13+
frames_per_block: int
14+
15+
@property
16+
def blocks(self):
17+
return self.frames // self.frames_per_block
18+
19+
@property
20+
def remainder_frames(self):
21+
return self.frames % self.frames_per_block
22+
23+
24+
def _get_frames_per_file_writer(
25+
frame_count: int, frames_per_block: int, n_file_writers: int
26+
) -> list[int]:
27+
n_blocks = math.ceil(frame_count / frames_per_block)
28+
min_blocks_per_file = n_blocks // n_file_writers
29+
remainder = n_blocks - min_blocks_per_file * n_file_writers
30+
31+
frames_per_file_writer = []
32+
for i in range(n_file_writers):
33+
blocks = min_blocks_per_file + (i < remainder)
34+
frames_per_file_writer.append(blocks * frames_per_block)
35+
36+
overflow = sum(frames_per_file_writer) - frame_count
37+
frames_per_file_writer[remainder - 1] -= overflow
38+
return frames_per_file_writer
39+
40+
41+
def _calculate_frame_distribution(
42+
frame_count, frames_per_block, blocks_per_file, n_file_writers
43+
) -> dict[int, FileFrames]:
44+
45+
frame_distribution: dict[int, FileFrames] = {}
46+
47+
max_frames_per_file = (
48+
frames_per_block * blocks_per_file if blocks_per_file else frame_count
49+
)
50+
# total frames written before one of the file writers has to start a new file
51+
frames_before_new_file = n_file_writers * max_frames_per_file
52+
frames_per_file_writer = _get_frames_per_file_writer(
53+
frame_count, frames_per_block, n_file_writers
54+
)
55+
for file_writer_idx, n_frames in enumerate(frames_per_file_writer):
56+
n_files = math.ceil(n_frames / max_frames_per_file)
57+
for i in range(n_files):
58+
file_idx = file_writer_idx + i * n_file_writers
59+
60+
frame_distribution[file_idx + 1] = FileFrames(
61+
frames=min(n_frames - i * max_frames_per_file, max_frames_per_file),
62+
frames_per_block=frames_per_block,
63+
start=frames_per_block * file_writer_idx
64+
+ file_idx // n_file_writers * frames_before_new_file,
65+
)
66+
67+
return frame_distribution
68+
69+
70+
def create_interleave_vds(
71+
path: Path,
72+
prefix: str,
73+
datasets: list[str],
74+
frame_count: int,
75+
frames_per_block: int,
76+
blocks_per_file: int,
77+
frame_shape: tuple[int, int],
78+
dtype: str = "float",
79+
n_file_writers: int = 4,
80+
) -> None:
81+
frame_distribution = _calculate_frame_distribution(
82+
frame_count, frames_per_block, blocks_per_file, n_file_writers
83+
)
84+
stride = n_file_writers * frames_per_block
85+
filepath = f"{path / prefix}_vds.h5"
86+
logger.info(f"Writing virtual dataset at {filepath}")
87+
with h5py.File(f"{path / prefix}_vds.h5", "w", libver="latest") as f:
88+
for dataset_name in datasets:
89+
v_layout = h5py.VirtualLayout(
90+
shape=(frame_count, frame_shape[0], frame_shape[1]),
91+
dtype=dtype,
92+
)
93+
for file_number, file_frames in frame_distribution.items():
94+
full_block_frames = file_frames.blocks * frames_per_block
95+
v_source = h5py.VirtualSource(
96+
f"{prefix}_{str(file_number).zfill(6)}.h5",
97+
name=dataset_name,
98+
shape=(file_frames.frames, frame_shape[0], frame_shape[1]),
99+
dtype=dtype,
100+
)
101+
if file_frames.blocks:
102+
source = v_source[:full_block_frames, :, :]
103+
v_layout[
104+
h5py.MultiBlockSlice(
105+
start=file_frames.start,
106+
stride=stride,
107+
count=file_frames.blocks,
108+
block=frames_per_block,
109+
),
110+
:,
111+
:,
112+
] = source
113+
if file_frames.remainder_frames:
114+
# Last few frames that don't fit into a block
115+
source = v_source[full_block_frames : file_frames.frames, :, :]
116+
v_layout[
117+
frame_count - file_frames.remainder_frames : frame_count, :, :
118+
] = source
119+
120+
f.create_virtual_dataset(dataset_name, v_layout)

tests/test_eiger_odin_controller.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from pathlib import Path
2+
13
import pytest
4+
from fastcs.attributes import AttrR, AttrRW
25
from fastcs.connections import IPConnectionSettings
6+
from fastcs.datatypes import Int, String
37
from pytest_mock import MockerFixture
48

59
from fastcs_eiger.controllers.eiger_controller import EigerController
@@ -8,13 +12,29 @@
812

913

1014
@pytest.fixture
11-
def eiger_odin_controller():
15+
def eiger_odin_controller(mocker: MockerFixture):
1216
detector_connection_settings = IPConnectionSettings("127.0.0.1", 8000)
1317
odin_connection_settings = IPConnectionSettings("127.0.0.1", 8001)
14-
return EigerOdinController(
18+
controller = EigerOdinController(
1519
detector_connection_settings, odin_connection_settings, api_version="1.8.0"
1620
)
1721

22+
controller.OD.file_path = AttrRW(String(), initial_value="/tmp/data") # pyright: ignore[reportAttributeAccessIssue]
23+
controller.OD.file_prefix = AttrRW(String(), initial_value="test_prefix") # pyright: ignore[reportAttributeAccessIssue]
24+
controller.OD.block_size = AttrRW(Int(), initial_value=4) # pyright: ignore[reportAttributeAccessIssue]
25+
26+
fp_mock = mocker.patch.object(controller.OD, "FP", create=True)
27+
fp_mock.data_compression.put = mocker.AsyncMock()
28+
fp_mock.data_datatype.put = mocker.AsyncMock()
29+
fp_mock.data_datatype.get.return_value = "uint16"
30+
fp_mock.frames = AttrRW(Int(), initial_value=100)
31+
fp_mock.process_blocks_per_file = AttrR(Int(), initial_value=10)
32+
fp_mock.data_dims_0 = AttrR(Int(), initial_value=512)
33+
fp_mock.data_dims_1 = AttrR(Int(), initial_value=1024)
34+
fp_mock.start_writing = mocker.AsyncMock()
35+
36+
return controller
37+
1838

1939
@pytest.mark.asyncio
2040
async def test_eiger_odin_controller(eiger_odin_controller, mocker: MockerFixture):
@@ -58,10 +78,9 @@ async def test_start_writing(eiger_odin_controller, mocker: MockerFixture):
5878
detector_mock.compression.get.return_value = "lz4"
5979
detector_mock.bit_depth_image.get.return_value = 16
6080

61-
fp_mock = mocker.patch.object(controller.OD, "FP", create=True)
62-
fp_mock.data_compression.put = mocker.AsyncMock()
63-
fp_mock.data_datatype.put = mocker.AsyncMock()
64-
fp_mock.start_writing = mocker.AsyncMock()
81+
create_mock = mocker.patch(
82+
"fastcs_eiger.controllers.odin.eiger_odin_controller.create_interleave_vds"
83+
)
6584

6685
writing_wait_mock = mocker.patch.object(controller.OD.writing, "wait_for_value")
6786

@@ -72,9 +91,25 @@ async def test_start_writing(eiger_odin_controller, mocker: MockerFixture):
7291
writing_wait_mock.side_effect = None
7392
await controller.start_writing()
7493

75-
fp_mock.data_compression.put.assert_awaited_with("LZ4")
76-
fp_mock.data_datatype.put.assert_awaited_with("uint16")
77-
fp_mock.start_writing.assert_awaited_with()
94+
controller.OD.FP.data_compression.put.assert_awaited_with("LZ4")
95+
controller.OD.FP.data_datatype.put.assert_awaited_with("uint16")
96+
controller.OD.FP.start_writing.assert_awaited_with()
7897
writing_wait_mock.assert_awaited_with(
7998
True, timeout=controller.start_writing_timeout.get()
8099
)
100+
101+
create_mock.assert_not_called()
102+
103+
controller.enable_vds_creation._value = True
104+
await controller.start_writing()
105+
106+
create_mock.assert_called_once_with(
107+
path=Path("/tmp/data"),
108+
prefix="test_prefix",
109+
datasets=["data", "data2", "data3"],
110+
frame_count=100,
111+
frames_per_block=4,
112+
blocks_per_file=10,
113+
frame_shape=(1024, 512),
114+
dtype="uint16",
115+
)

0 commit comments

Comments
 (0)