Storage¶
Warning
Storage support is under active development. Expect breaking changes.
redsun provides a session-scoped storage layer that lets devices write acquired
frames to disk without managing their own file handles or knowing where data
lands.
Overview¶
Writers are injected into devices at construction time via dependency injection. Multiple devices that should write into the same store receive the same writer instance — the writer tracks each device as a named source and fans their frames into a single backend.
graph TD
DI[Application / DI container] -->|"ZarrWriter('default')"| W[ZarrWriter]
W --> |writer_logic| DA[Device A]
W --> |writer_logic| DB[Device B]
DC[Device C\nno storage] -. unaffected .-> W
SP[Presenter] -->|set_uri + clear_sources| W
Storage is opt-in per device — devices that do not receive a writer are unaffected. The writer instance itself is stateless between acquisitions: it is opened, written to, and closed on each plan, then reused for the next.
Protocol hierarchy¶
Two protocols govern writer objects:
| Protocol | Purpose |
|---|---|
[DataWriter][redsun.device.DataWriter] |
Single-device persistence: open, write, collect documents, close |
[ControllableDataWriter][redsun.device.ControllableDataWriter] |
Extends DataWriter with source registration, frame pushing, and URI configuration; satisfied by shared backends like ZarrWriter |
A device that holds a writer exposes it through the
[HasWriterLogic][redsun.storage.HasWriterLogic] protocol (a writer_logic
property typed as DataWriter). Helper functions and callbacks use this
protocol to discover writers without depending on any concrete class.
Device side¶
An imaging device receives a writer at __init__ time and uses it in
prepare and kickoff:
from __future__ import annotations
import numpy as np
from bluesky.protocols import Status
from redsun.device import Device, ControllableDataWriter, PrepareInfo
class MyCamera(Device):
def __init__(
self,
name: str,
/,
writer: ControllableDataWriter,
) -> None:
super().__init__(name)
self._writer = writer
@property
def writer_logic(self) -> ControllableDataWriter:
"""Expose the writer so presenters and callbacks can discover it."""
return self._writer
def prepare(self, value: PrepareInfo) -> Status:
s = Status()
try:
capacity = 0 if value.write_forever else value.capacity
self._writer.register(
name=self.name,
dtype=np.dtype("uint16"),
shape=(2048, 2048),
capacity=capacity,
)
except Exception as e:
s.set_exception(e)
else:
s.set_finished()
return s
def kickoff(self) -> Status:
s = Status()
try:
self._writer.open(self.name)
# arm hardware, start acquisition loop …
except Exception as e:
s.set_exception(e)
else:
s.set_finished()
return s
The writer_logic property makes the device satisfy
[HasWriterLogic][redsun.storage.HasWriterLogic] structurally, enabling
automatic discovery by the presenter and metadata callbacks without any
explicit registration.
Metadata¶
There are two ways to attach metadata to a store.
Direct — in prepare()¶
Call [update_metadata(metadata)][redsun.storage.Writer.update_metadata] on the
writer directly. This is the simplest option when the device owns the writer:
def prepare(self, value: PrepareInfo) -> Status:
self._writer.update_metadata({
self.name: {
"exposure_time": self._exposure_s,
"roi": list(self._roi),
}
})
self._writer.register(self.name, ...)
...
Metadata accumulates on the writer between prepare() calls and is passed
into the backend when the store is first opened. It is cleared automatically
by [close()][redsun.storage.Writer.close] (or explicitly via
[clear_metadata()][redsun.storage.Writer.clear_metadata]) so each new run
starts clean.
Via a bluesky descriptor callback¶
For devices that are not imaging devices (motors, lights, etc.) a bluesky
callback can forward the configuration section of each
EventDescriptor
document into the associated writer using
[handle_descriptor_metadata][redsun.storage.handle_descriptor_metadata]:
from redsun.storage import handle_descriptor_metadata
class MyCallback:
def __init__(self, devices: dict) -> None:
self._devices = devices
def descriptor(self, doc) -> None:
handle_descriptor_metadata(doc, self._devices)
# … rest of callback logic
The function iterates the descriptor's configuration section, looks up each
device name in devices, checks whether it satisfies HasWriterLogic and
whether its writer satisfies [HasMetadata][redsun.storage.HasMetadata]
(i.e. implements update_metadata), and forwards the configuration snapshot.
Bluesky emits one descriptor per stream name per run, so this function fires once per run for a given stream.
Presenter side¶
Before each plan the presenter must set the write location on every active
writer. [get_available_writers][redsun.storage.presenter.get_available_writers]
discovers unique writers from the device registry via HasWriterLogic:
from pathlib import Path
from redsun.storage import SessionPathProvider
from redsun.storage.presenter import get_available_writers
# devices is your application device registry (Mapping[str, Any])
writers = get_available_writers(devices)
# {"application/x-zarr": {"default": <ZarrWriter>}}
provider = SessionPathProvider(base_dir=Path("/data"), session="exp1")
for mimetype, groups in writers.items():
for group_name, writer in groups.items():
path_info = provider(plan_name, group=group_name)
writer.clear_sources()
writer.set_uri(path_info.store_uri)
SessionPathProvider produces auto-incrementing URIs of the form:
The counter increments independently per plan name, so different plans never collide. Counters for existing directories are picked up automatically on construction, preventing overwrites across application restarts.
Backend dependencies¶
The Zarr backend requires the optional acquire-zarr package:
The import is deferred — sessions without imaging devices have no dependency
on acquire-zarr.