Skip to content
Open
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
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
workflow_dispatch:
pull_request:
push:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -32,18 +34,13 @@ jobs:
pipx run nox -s pylint

checks:
# pull requests are a duplicate of a branch push if within the same repo.
if:
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name != github.repository

name: Check Python ${{ matrix.python-version }} on ${{ matrix.runs-on }}
runs-on: ${{ matrix.runs-on }}
needs: [pre-commit]
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12"]
# runs-on: [ubuntu-latest, macos-latest, windows-latest]
runs-on: [ubuntu-latest]

Expand Down
15 changes: 8 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ ci:

repos:
- repo: https://github.com/adamchainz/blacken-docs
rev: "1.16.0"
rev: "1.20.0"
hooks:
- id: blacken-docs
additional_dependencies: [black==23.*]

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: "v4.6.0"
rev: "v6.0.0"
hooks:
- id: check-added-large-files
- id: check-case-conflict
Expand Down Expand Up @@ -40,7 +40,7 @@ repos:
args: [--prose-wrap=always]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.4.5"
rev: "v0.15.16"
hooks:
- id: ruff
args: ["--fix", "--show-fixes"]
Expand All @@ -56,12 +56,13 @@ repos:
# - pytest

- repo: https://github.com/codespell-project/codespell
rev: "v2.3.0"
rev: "v2.4.2"
hooks:
- id: codespell
args: ["--ignore-words-list=astroid"]

- repo: https://github.com/shellcheck-py/shellcheck-py
rev: "v0.10.0.1"
rev: "v0.11.0.1"
hooks:
- id: shellcheck

Expand All @@ -74,13 +75,13 @@ repos:
exclude: .pre-commit-config.yaml

- repo: https://github.com/abravalheri/validate-pyproject
rev: "v0.18"
rev: "v0.25"
hooks:
- id: validate-pyproject
additional_dependencies: ["validate-pyproject-schema-store[all]"]

- repo: https://github.com/python-jsonschema/check-jsonschema
rev: "0.28.4"
rev: "0.37.2"
hooks:
- id: check-dependabot
- id: check-github-workflows
Expand Down
7,295 changes: 7,295 additions & 0 deletions pixi.lock

Large diffs are not rendered by default.

41 changes: 35 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["hatchling", "hatch-vcs", "setuptools>=61,<67"]
requires = ["hatchling", "hatch-vcs", "setuptools>=61"]
build-backend = "hatchling.build"


Expand All @@ -11,7 +11,7 @@ authors = [
description = "Caproto IOCs for the NSLS-II SRX beamline"
readme = "README.md"
license.file = "LICENSE"
requires-python = ">=3.8"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 1 - Planning",
"Intended Audience :: Science/Research",
Expand All @@ -21,8 +21,6 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -44,6 +42,7 @@ dependencies = [
test = [
"pytest >=6",
"pytest-cov >=3",
"pytest-timeout >=2",
]
dev = [
"ipython",
Expand Down Expand Up @@ -79,10 +78,11 @@ scripts.test = "pytest {args}"

[tool.pytest.ini_options]
minversion = "6.0"
addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"]
addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config", "--timeout=20"]
xfail_strict = true
filterwarnings = [
"error",
"ignore:SelectableGroups dict interface is deprecated:DeprecationWarning",
]
log_cli_level = "INFO"
testpaths = [
Expand All @@ -104,7 +104,7 @@ report.exclude_also = [

[tool.mypy]
files = ["src", "tests"]
python_version = "3.8"
python_version = "3.10"
warn_unused_configs = true
strict = true
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
Expand Down Expand Up @@ -171,3 +171,32 @@ messages_control.disable = [
"missing-module-docstring",
"wrong-import-position",
]

[tool.pixi.workspace]
channels = ["conda-forge"]
platforms = ["osx-arm64", "linux-64"]

[tool.pixi.feature.dev.pypi-dependencies]
srx-caproto-iocs = { path = ".", editable = true }

[tool.pixi.feature.test.pypi-dependencies]
srx-caproto-iocs = { path = ".", editable = true, extras = ["test"] }

[tool.pixi.environments]
default = { solve-group = "default" }
dev = { features = ["dev"], solve-group = "default" }
docs = { features = ["docs"], solve-group = "default" }
test = { features = ["test"], solve-group = "default" }

[tool.pixi.tasks]
test = { cmd = "pytest", description = "Run the test suite" }

[tool.pixi.dependencies]
pip = ">=26.1.1,<27"

[tool.pixi.feature.dev.dependencies]
pre-commit = ">=4.6.0,<5"
pytest = ">=9.0.3,<10"
python = "3.12.*"
nexpy = ">=2.0.1,<3"
epics-base = ">=7.0.9.0,<8"
10 changes: 5 additions & 5 deletions src/srx_caproto_iocs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ def saver(request_queue, response_queue):
data = received["data"]
frame_number = received["frame_number"]
try:
save_hdf5_nd(fname=filename, data=data, mode="x", group_path="enc1")
save_hdf5_nd(
fname=filename, data=data, mode="a", group_path="data/data"
)
print(
f"{now()}: saved {frame_number=} {data.shape} data into:\n {filename}"
)
Expand Down Expand Up @@ -272,13 +274,11 @@ def set(self, command):
def cb(value, old_value, **kwargs):
# pylint: disable=unused-argument
# print(f"{now()}: {old_value} -> {value}")
if value == expected_new_value and old_value == expected_old_value:
return True
return False
return value == expected_new_value and old_value == expected_old_value

st = SubscriptionStatus(obj, callback=cb, run=False)
# print(f"{now()}: {cmd = }")
obj.put(cmd)
obj.put(cmd, timeout=10)
return st


Expand Down
91 changes: 61 additions & 30 deletions src/srx_caproto_iocs/zebra/caproto_ioc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# pylint: disable=duplicate-code
from __future__ import annotations

import json
import textwrap
from enum import Enum

Expand Down Expand Up @@ -163,22 +164,53 @@ class ZebraSaveIOC(CaprotoSaveIOC):
# super().__init__(*args, **kwargs)
# self._external_pvs = external_pvs

#: Default dataset mappings keyed by dev_type. Keys are PV attribute names;
#: values are the corresponding HDF5 dataset names written to file.
_DEFAULT_DATASET_MAPS: dict[str, dict[str, str]] = {
DevTypes.ZEBRA.value: {
"enc1": "enc1",
"enc2": "enc2",
"enc3": "enc3",
"zebra_time": "zebra_time",
},
DevTypes.SCALER.value: {
"i0": "i0",
"im": "im",
"it": "it",
"sis_time": "sis_time",
},
}

def __init__(
self,
*args,
dataset_map: dict[str, str] | None = None,
**kwargs,
):
"""Init method.

Parameters
----------
dataset_map : dict, optional
Mapping of PV attribute names to HDF5 dataset names, e.g.
``{"enc1": "x_pos", "enc2": "y_pos"}``. When *None* (default)
the mapping is chosen automatically based on the ``dev_type`` PV.
"""
self._dataset_map = dataset_map
super().__init__(*args, **kwargs)

async def _get_current_dataset(self, *args, **kwargs): # pylint: disable=unused-argument
# , frame, external_pv="enc1"):
# client_context = Context()
# (pvobject,) = await client_context.get_pvs(self._external_pvs[external_pv])
# print(f"{pvobject = }")
# # pvobject = pvobjects[0]
# ret = await pvobject.read()

if self.dev_type.value == DevTypes.ZEBRA.value:
pvnames = ["enc1", "enc2", "enc3", "zebra_time"]
if self._dataset_map is not None:
mapping = self._dataset_map
elif self.dev_type.value == DevTypes.ZEBRA.value:
mapping = self._DEFAULT_DATASET_MAPS[DevTypes.ZEBRA.value]
else:
pvnames = ["i0", "im", "it", "sis_time"]
mapping = self._DEFAULT_DATASET_MAPS[DevTypes.SCALER.value]

dataset = {}
for pvname in pvnames:
dataset[pvname] = getattr(self, pvname).value
dataset = {
hdf5_name: getattr(self, pv_attr).value
for pv_attr, hdf5_name in mapping.items()
}

print(f"{now()}:\n{dataset}")

Expand All @@ -193,7 +225,7 @@ def saver(request_queue, response_queue):
data = received["data"]
# 'frame_number' is not used for this exporter.
try:
save_hdf5_zebra(fname=filename, data=data, mode="x")
save_hdf5_zebra(fname=filename, data=data, mode="a")
print(f"{now()}: saved data into:\n {filename}")

success = True
Expand All @@ -213,22 +245,21 @@ def saver(request_queue, response_queue):
parser, split_args = template_arg_parser(
default_prefix="", desc=textwrap.dedent(ZebraSaveIOC.__doc__)
)

parser.add_argument(
"--dataset-map",
help=(
"JSON mapping of PV attribute names to HDF5 dataset names, "
'e.g. \'{"enc1": "x_pos", "enc2": "y_pos"}\'. '
"When omitted, the mapping is chosen from the built-in SRX defaults "
"based on the dev_type PV (zebra or scaler)."
),
type=json.loads,
default=None,
)

ioc_options, run_options = check_args(parser, split_args)
dataset_map_arg = parser.parse_args().dataset_map

# external_pv_prefix = (
# ioc_options["prefix"].replace("{{", "{").replace("}}", "}")
# ) # "XF:05IDD-ES:1{Dev:Zebra2}:"

# external_pvs = {
# "pulse_step": external_pv_prefix + "PC_PULSE_STEP",
# "data_in_progress": external_pv_prefix + "ARRAY_ACQ",
# "enc1": external_pv_prefix + "PC_ENC1",
# "enc2": external_pv_prefix + "PC_ENC2",
# "enc3": external_pv_prefix + "PC_ENC3",
# "enc4": external_pv_prefix + "PC_ENC4",
# "time": external_pv_prefix + "PC_TIME",
# }

# ioc = ZebraSaveIOC(external_pvs=external_pvs, **ioc_options)
ioc = ZebraSaveIOC(**ioc_options)
ioc = ZebraSaveIOC(dataset_map=dataset_map_arg, **ioc_options)
run(ioc.pvdb, **run_options)
18 changes: 18 additions & 0 deletions src/srx_caproto_iocs/zebra/ophyd.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
from __future__ import annotations

from ophyd import Component as Cpt
from ophyd import EpicsSignal, EpicsSignalRO

from ..base import OphydDeviceWithCaprotoIOC


class ZebraWithCaprotoIOC(OphydDeviceWithCaprotoIOC):
"""An ophyd Device which works with the Zebra caproto extension IOC."""

# Device-type selector (zebra / scaler)
dev_type = Cpt(EpicsSignal, "dev_type", string=True)

# Zebra position-capture channels
enc1 = Cpt(EpicsSignalRO, "enc1", auto_monitor=False)
enc2 = Cpt(EpicsSignalRO, "enc2", auto_monitor=False)
enc3 = Cpt(EpicsSignalRO, "enc3", auto_monitor=False)
zebra_time = Cpt(EpicsSignalRO, "zebra_time", auto_monitor=False)

# Scaler channels
i0 = Cpt(EpicsSignalRO, "i0", auto_monitor=False)
im = Cpt(EpicsSignalRO, "im", auto_monitor=False)
it = Cpt(EpicsSignalRO, "it", auto_monitor=False)
sis_time = Cpt(EpicsSignalRO, "sis_time", auto_monitor=False)
Loading
Loading