Skip to content

Commit 3edcc06

Browse files
authored
Implement Waveform (#247)
1 parent 9ebdf4d commit 3edcc06

File tree

8 files changed

+87
-42
lines changed

8 files changed

+87
-42
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ requires-python = ">=3.11"
3030

3131
[project.optional-dependencies]
3232
demo = ["tickit~=0.4.3"]
33-
epicsca = ["pvi~=0.11.0", "softioc>=4.5.0"]
34-
epicspva = ["p4p", "pvi~=0.11.0"]
33+
epicsca = ["pvi~=0.12.0", "softioc>=4.5.0"]
34+
epicspva = ["p4p", "pvi~=0.12.0"]
3535
epics = ["fastcs[epicsca]", "fastcs[epicspva]"]
3636
tango = ["pytango"]
3737
graphql = ["strawberry-graphql", "uvicorn[standard]>=0.12.0"]

src/fastcs/datatypes/waveform.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def initial_value(self) -> np.ndarray:
2020
return np.zeros(self.shape, dtype=self.array_dtype)
2121

2222
def validate(self, value: np.ndarray) -> np.ndarray:
23-
_value = super().validate(value)
23+
_value = super().validate(np.asarray(value).astype(self.array_dtype))
2424

2525
if self.array_dtype != _value.dtype:
2626
raise ValueError(

src/fastcs/demo/controllers.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from dataclasses import KW_ONLY, dataclass
55
from typing import TypeVar
66

7+
import numpy as np
8+
79
from fastcs.attributes import AttributeIO, AttributeIORef, AttrR, AttrRW, AttrW
810
from fastcs.connections import IPConnection, IPConnectionSettings
911
from fastcs.controllers import Controller
10-
from fastcs.datatypes import Enum, Float, Int
12+
from fastcs.datatypes import Enum, Float, Int, Waveform
1113
from fastcs.methods import command, scan
1214

1315
NumberT = TypeVar("NumberT", int, float)
@@ -66,6 +68,7 @@ async def update(
6668
class TemperatureController(Controller):
6769
ramp_rate = AttrRW(Float(), io_ref=TemperatureControllerAttributeIORef(name="R"))
6870
power = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef(name="P"))
71+
voltages = AttrR(Waveform(np.int32, shape=(4,)))
6972

7073
def __init__(self, settings: TemperatureControllerSettings) -> None:
7174
self.connection = IPConnection()
@@ -101,6 +104,9 @@ async def update_voltages(self):
101104
voltages = json.loads(
102105
(await self.connection.send_query(f"{query}\r\n")).strip("\r\n")
103106
)
107+
108+
await self.voltages.update(voltages)
109+
104110
for index, controller in enumerate(self._ramp_controllers):
105111
self.log_event(
106112
"Update voltages",

src/fastcs/transports/epics/gui.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pvi._format.dls import DLSFormatter # type: ignore
22
from pvi.device import (
33
LED,
4+
ArrayTrace,
45
ButtonPanel,
56
ComboBox,
67
ComponentUnion,
@@ -66,8 +67,12 @@ def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None:
6667
return TextRead(format=TextFormat.string)
6768
case Enum():
6869
return TextRead(format=TextFormat.string)
69-
case Waveform():
70-
return None
70+
case Waveform() as waveform:
71+
if len(waveform.shape) > 1:
72+
logger.warning("EPICS CA transport only supports 1D waveforms")
73+
return None
74+
75+
return ArrayTrace(axis="x")
7176
case datatype:
7277
raise TypeError(f"Unsupported type {type(datatype)}: {datatype}")
7378

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from pvi.device import (
22
CheckBox,
3+
ImageColorMap,
4+
ImageRead,
35
ReadWidgetUnion,
46
TableRead,
57
TableWrite,
68
WriteWidgetUnion,
79
)
810

9-
from fastcs.datatypes import Bool, DataType, Table, numpy_to_fastcs_datatype
11+
from fastcs.datatypes import Bool, DataType, Table, Waveform, numpy_to_fastcs_datatype
1012
from fastcs.transports.epics.gui import EpicsGUI
1113

1214

@@ -18,31 +20,39 @@ class PvaEpicsGUI(EpicsGUI):
1820
def _get_pv(self, attr_path: list[str], name: str):
1921
return f"pva://{super()._get_pv(attr_path, name)}"
2022

21-
def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None: # noqa: F821
22-
if isinstance(fastcs_datatype, Table):
23-
fastcs_datatypes = [
24-
numpy_to_fastcs_datatype(datatype)
25-
for _, datatype in fastcs_datatype.structured_dtype
26-
]
27-
28-
base_get_read_widget = super()._get_read_widget
29-
widgets = [base_get_read_widget(datatype) for datatype in fastcs_datatypes]
30-
31-
return TableRead(widgets=widgets) # type: ignore
32-
else:
33-
return super()._get_read_widget(fastcs_datatype)
23+
def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None:
24+
match fastcs_datatype:
25+
case Table():
26+
fastcs_datatypes = [
27+
numpy_to_fastcs_datatype(datatype)
28+
for _, datatype in fastcs_datatype.structured_dtype
29+
]
30+
31+
base_get_read_widget = super()._get_read_widget
32+
widgets = [
33+
base_get_read_widget(datatype) for datatype in fastcs_datatypes
34+
]
35+
36+
return TableRead(widgets=widgets) # type: ignore
37+
case Waveform(shape=(height, width)):
38+
return ImageRead(
39+
height=height, width=width, color_map=ImageColorMap.GRAY
40+
)
41+
case _:
42+
return super()._get_read_widget(fastcs_datatype)
3443

3544
def _get_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | None:
36-
if isinstance(fastcs_datatype, Table):
37-
widgets = []
38-
for _, datatype in fastcs_datatype.structured_dtype:
39-
fastcs_datatype = numpy_to_fastcs_datatype(datatype)
40-
if isinstance(fastcs_datatype, Bool):
41-
# Replace with compact version for Table row
42-
widget = CheckBox()
43-
else:
44-
widget = super()._get_write_widget(fastcs_datatype)
45-
widgets.append(widget)
46-
return TableWrite(widgets=widgets)
47-
else:
48-
return super()._get_write_widget(fastcs_datatype)
45+
match fastcs_datatype:
46+
case Table():
47+
widgets = []
48+
for _, datatype in fastcs_datatype.structured_dtype:
49+
fastcs_datatype = numpy_to_fastcs_datatype(datatype)
50+
if isinstance(fastcs_datatype, Bool):
51+
# Replace with compact version for Table row
52+
widget = CheckBox()
53+
else:
54+
widget = super()._get_write_widget(fastcs_datatype)
55+
widgets.append(widget)
56+
return TableWrite(widgets=widgets)
57+
case _:
58+
return super()._get_write_widget(fastcs_datatype)

tests/test_datatypes.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ class MyIntEnum(IntEnum):
3434
(Float, {"min": 1}, 0.0),
3535
(Float, {"max": -1}, 0.0),
3636
(Enum, {"enum_cls": int}, 0),
37-
(Waveform, {"array_dtype": "U64", "shape": (1,)}, np.ndarray([1])),
38-
(Waveform, {"array_dtype": "float64", "shape": (1, 1)}, np.ndarray([1])),
37+
(Waveform, {"array_dtype": "uint64", "shape": (1, 1)}, np.ndarray([1])),
3938
],
4039
)
4140
def test_validate(datatype, init_args, value):

tests/transports/epics/ca/test_gui.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
from pvi.device import (
44
LED,
5+
ArrayTrace,
56
ButtonPanel,
67
ComboBox,
78
Group,
@@ -39,7 +40,7 @@ def test_get_pv():
3940
(Float(), TextRead()),
4041
(String(), TextRead(format=TextFormat.string)),
4142
(Enum(ColourEnum), TextRead(format=TextFormat.string)),
42-
# (Waveform(array_dtype=np.int32), None),
43+
(Waveform(array_dtype=np.int32), ArrayTrace(axis="x")),
4344
],
4445
)
4546
def test_get_attribute_component_r(datatype, widget):
@@ -50,6 +51,18 @@ def test_get_attribute_component_r(datatype, widget):
5051
)
5152

5253

54+
@pytest.mark.parametrize(
55+
"datatype",
56+
[
57+
(Waveform(array_dtype=np.int32, shape=(10, 10))),
58+
],
59+
)
60+
def test_get_attribute_component_r_signal_none(datatype):
61+
gui = EpicsGUI(ControllerAPI(), "DEVICE")
62+
63+
assert gui._get_attribute_component([], "Attr", AttrR(datatype)) is None
64+
65+
5366
@pytest.mark.parametrize(
5467
"datatype, widget",
5568
[
@@ -78,11 +91,6 @@ def test_get_attribute_component_none(mocker):
7891
assert gui._get_attribute_component([], "Attr", AttrRW(Int())) is None
7992

8093

81-
def test_get_read_widget_none():
82-
gui = EpicsGUI(ControllerAPI(), "DEVICE")
83-
assert gui._get_read_widget(fastcs_datatype=Waveform(np.int32)) is None
84-
85-
8694
def test_get_write_widget_none():
8795
gui = EpicsGUI(ControllerAPI(), "DEVICE")
8896
assert gui._get_write_widget(fastcs_datatype=Waveform(np.int32)) is None

tests/transports/epics/pva/test_pva_gui.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import numpy as np
2+
import pytest
23
from pvi.device import (
34
LED,
45
ButtonPanel,
56
CheckBox,
7+
ImageRead,
68
SignalR,
79
SignalW,
810
SignalX,
@@ -14,11 +16,26 @@
1416
)
1517

1618
from fastcs.attributes import AttrR, AttrW
17-
from fastcs.datatypes import Table
19+
from fastcs.datatypes import Table, Waveform
1820
from fastcs.transports import ControllerAPI
21+
from fastcs.transports.epics.gui import EpicsGUI
1922
from fastcs.transports.epics.pva.gui import PvaEpicsGUI
2023

2124

25+
@pytest.mark.parametrize(
26+
"datatype, widget",
27+
[
28+
(Waveform(array_dtype=np.int32), ImageRead()),
29+
],
30+
)
31+
def test_pva_get_attribute_component_r(datatype, widget):
32+
gui = EpicsGUI(ControllerAPI(), "DEVICE")
33+
34+
assert gui._get_attribute_component([], "Attr", AttrR(datatype)) == SignalR(
35+
name="Attr", read_pv="Attr", read_widget=widget
36+
)
37+
38+
2239
def test_get_pv_in_pva():
2340
gui = PvaEpicsGUI(ControllerAPI(), "DEVICE")
2441

0 commit comments

Comments
 (0)