Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8,923 changes: 2,877 additions & 6,046 deletions pixi.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ python = ">=3.11,<3.14"
ipython = ">=9.16.1,<10"
tiled-client = ">=0.2.14,<0.3"
bluesky-tiled-plugins = ">=2.0.11,<3"
ophyd-async = ">=0.21.3,<0.22"
nslsii = ">=0.11.9,<0.12"
redis-json-dict = ">=0.2.2,<0.3"
rich = "*"

[pypi-dependencies]
pdftools = { path = ".", editable = true }
# ContAcqDetector proc_suffix (ophyd-async #1329) and NSLS2PathProvider base_write_dir
# (nslsii #231) are unreleased; same commits as hextools.
ophyd-async = { git = "https://github.com/bluesky/ophyd-async", rev = "de097ace33ce2fa0ea2eb22572ef81e5af133b16", extras = ["ca", "pva", "sim"] }
nslsii = { git = "https://github.com/NSLS2/nslsii", rev = "b8c69931660f2d74534f44c210499958959a1a0e" }

[feature.profile.dependencies]

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ dependencies = [
"bluesky",
"tiled[client]",
"bluesky-tiled-plugins>=2.0.11",
"ophyd-async[ca, pva, sim]>=0.21.3",
"nslsii",
"ophyd-async[ca, pva, sim] @ git+https://github.com/bluesky/ophyd-async@de097ace33ce2fa0ea2eb22572ef81e5af133b16",
"nslsii @ git+https://github.com/NSLS2/nslsii@b8c69931660f2d74534f44c210499958959a1a0e",
Comment thread
Copilot marked this conversation as resolved.
Resolved
"rich",
]

Expand Down
4 changes: 2 additions & 2 deletions src/pdftools/detectors/xspd.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
ADWriterFactory,
AreaDetector,
NDPluginBaseIO,
trigger_info_from_num_images,
trigger_info_from_driver,
)
from ophyd_async.epics.core import (
EpicsDevice,
Expand Down Expand Up @@ -238,7 +238,7 @@ async def prepare_internal(self, num: int, livetime: float, deadtime: float):
await asyncio.gather(*coros)

async def default_trigger_info(self):
return await trigger_info_from_num_images(self.driver)
return await trigger_info_from_driver(self.driver.num_images)


class XSPDetector(AreaDetector[XSPIO]):
Expand Down
26 changes: 25 additions & 1 deletion src/pdftools/profiles/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import argparse
import os
from pathlib import PureWindowsPath

import numpy as np
from bluesky import plan_stubs as bps
Expand All @@ -15,7 +16,7 @@
from IPython.core.getipython import get_ipython
from IPython.terminal.interactiveshell import TerminalInteractiveShell
from nslsii.ophyd_async.providers import NSLS2PathProvider
from ophyd_async.epics.adcore import ADWriterFactory
from ophyd_async.epics.adcore import ADWriterFactory, ContAcqDetector
from ophyd_async.epics.adpilatus import PilatusDetector
from tiled.client import from_uri, simple

Expand Down Expand Up @@ -46,6 +47,8 @@
# Beamline Redis for RunEngine metadata, as pdf-profile-collection's 00-base uses it
# (port 6380 with SSL is open_redis_client's default when redis_ssl=True).
os.environ.setdefault("REDIS_HOST", "xf28id1-pdf-redis1.nsls2.bnl.gov")
# NSLS2PathProvider derives /nsls2/data/<acronym>/proposals from this.
os.environ.setdefault("BEAMLINE_ACRONYM", "PDF")
# Have ophyd-async keep detector state constant when not explicitly overridden.
os.environ["OPHYD_ASYNC_PRESERVE_DETECTOR_STATE"] = "YES"

Expand Down Expand Up @@ -115,6 +118,14 @@
# Define our global default path provider for the beamline
path_provider = NSLS2PathProvider(RE.md)

# The Perkin Elmer IOCs run on Windows hosts and write to the proposals share as J:.
pe_path_provider = NSLS2PathProvider(
RE.md,
base_write_dir=PureWindowsPath("J:\\proposals"),
# Else the Windows root sets "\\" for the read-side date dirs too.
separator="/",
)

# Devices are added here, step by step (detectors, motors, sample environment).
with auto_init_devices(timeout=2.0):
# Area detectors writing TIFF, as in pdf-profile-collection (81-pilatus, 84-lambda)
Expand All @@ -129,6 +140,19 @@
ADWriterFactory.tiff(path_provider),
name="pilatus-1",
)
# Free-running flat panels; Proc1 sums frames per point, as pe1c/pe2c do today.
pe1c = ContAcqDetector(
"XF:28ID1-ES{Det:PE1}",
ADWriterFactory.tiff(pe_path_provider),
name="pe1",
proc_suffix="Proc1:",
)
pe2c = ContAcqDetector(
"XF:28ID1-ES{Det:PE2}",
ADWriterFactory.tiff(pe_path_provider),
name="pe2",
proc_suffix="Proc1:",
)

# Add baseline=[<device1>, <device2>, ...] to SupplementalData as devices arrive.
sd = bpp.SupplementalData()
Expand Down
73 changes: 73 additions & 0 deletions tests/test_detectors.py/test_perkin_elmer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import datetime
from pathlib import PurePosixPath, PureWindowsPath

import bluesky.plans as bp
import pytest
from bluesky.run_engine import RunEngine
from nslsii.ophyd_async.providers import NSLS2PathProvider
from ophyd_async.core import (
callback_on_mock_put,
get_mock_put,
init_devices,
set_mock_value,
)
from ophyd_async.epics.adcore import (
ADBaseDataType,
ADImageMode,
ADWriterFactory,
ContAcqDetector,
)


def test_perkin_elmer_count_sums_in_proc_and_writes_tiff_to_windows_share(
RE: RunEngine, monkeypatch: pytest.MonkeyPatch
):
# As the profile sets it: a count keeps the frame count summed in the IOC's Proc1.
monkeypatch.setenv("OPHYD_ASYNC_PRESERVE_DETECTOR_STATE", "YES")
RE.md.update({"cycle": "2026-3", "data_session": "pass-123456"})
path_provider = NSLS2PathProvider(
RE.md,
base_data_dir=PurePosixPath("/nsls2/data/pdf/proposals"),
base_write_dir=PureWindowsPath("J:\\proposals"),
separator="/",
)
with init_devices(mock=True):
pe = ContAcqDetector(
"TEST:PE2:",
ADWriterFactory.tiff(path_provider),
name="pe2",
proc_suffix="Proc1:",
)
set_mock_value(pe.driver.image_mode, ADImageMode.CONTINUOUS)
set_mock_value(pe.driver.acquire, True)
set_mock_value(pe.driver.array_size_x, 4)
set_mock_value(pe.driver.array_size_y, 3)
set_mock_value(pe.driver.data_type, ADBaseDataType.UINT16)
set_mock_value(pe.tiff.file_path_exists, True) # pyright: ignore[reportAttributeAccessIssue]
set_mock_value(pe.proc.enable_filter, True) # pyright: ignore[reportAttributeAccessIssue]
set_mock_value(pe.proc.num_filter, 5) # pyright: ignore[reportAttributeAccessIssue]

def _on_cb_trigger(value, **kwargs):
if value:
set_mock_value(pe.tiff.num_captured, 1) # pyright: ignore[reportAttributeAccessIssue]
return False

callback_on_mock_put(pe.cb.trigger_, _on_cb_trigger) # pyright: ignore[reportAttributeAccessIssue]

docs: dict[str, list] = {}
RE(bp.count([pe]), lambda name, doc: docs.setdefault(name, []).append(doc))

written_to = get_mock_put(pe.tiff.file_path).call_args.args[0] # pyright: ignore[reportAttributeAccessIssue]
assert written_to.startswith("J:\\proposals\\2026-3\\pass-123456\\")
sres = docs["stream_resource"][0]
assert sres["mimetype"] == "multipart/related;type=image/tiff"
# The read side is POSIX: no Windows separators may leak in from the write root.
today = datetime.date.today()
assert sres["uri"] == (
"file://localhost/nsls2/data/pdf/proposals/2026-3/pass-123456/"
f"assets/pe2/{today:%Y/%m/%d}/"
)
get_mock_put(pe.proc.num_filter).assert_called_with(5) # pyright: ignore[reportAttributeAccessIssue]
desc = docs["descriptor"][0]["data_keys"]["pe2"]
assert desc["shape"] == [1, 3, 4]
assert desc["dtype_numpy"] == "<u2"
2 changes: 1 addition & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,5 @@ def test_profile_boots_in_mock_mode(tmp_path: Path):
assert result.returncode == 0, result.stderr
assert "Mock mode proposal" in result.stdout
assert "Connecting to beamline devices" in result.stdout
for name in ("lambda1", "pilatus1"):
for name in ("lambda1", "pilatus1", "pe1c", "pe2c"):
assert re.search(rf"{re.escape(name)} \.+ \[\s*OK\s*\]", result.stdout), name
Loading