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
2 changes: 1 addition & 1 deletion docs/explanations/what-is-pytac.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ with EPICS, readback (``pytac.RB``) or setpoint (``pytac.SP``).
Data may be set to or retrieved from different data sources, from the live
machine (``pytac.LIVE``) or from a simulator (``pytac.SIM``). By default the
'live' data source is implemented using
`Cothread <https://github.com/dls-controls/cothread>`_ to communicate with
`aioca <https://github.com/dls-controls/aioca>`_ to communicate with
EPICS, as described above. The 'simulation' data source is left unimplemented,
as Pytac does not include a simulator. However, ATIP, a module designed to
integrate the `Accelerator Toolbox <https://github.com/atcollab/at>`_ simulator
Expand Down
4 changes: 2 additions & 2 deletions docs/tutorials/basic-tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ In this tutorial we will go through some of the most common ways of using pytac.
The aim is to give you an understanding of the interface and how to find out what
is available.

The import of the cothread channel access library and epicscorelibs will
The import of the aioca channel access library and epicscorelibs will
allow us to get some live values from the Diamond accelerators.

$ pip install cothread epicscorelibs
$ pip install aioca epicscorelibs

These docs are able to be run and tested, but may return different values as
accelerator conditions will have changed.
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ description = "Python Toolkit for Accelerator Controls (Pytac) is a Python libra
dependencies = [
"numpy",
"scipy",
"cothread",
"aioca",
"epicscorelibs",
] # Add project dependencies here, e.g. ["click", "numpy"]
dynamic = ["version"]
Expand All @@ -32,6 +32,7 @@ dev = [
"pre-commit",
"pydata-sphinx-theme>=0.12",
"pytest",
"pytest-asyncio>=0.17",
"pytest-cov",
"ruff",
"sphinx-autobuild",
Expand Down Expand Up @@ -68,6 +69,8 @@ addopts = """
filterwarnings = "error"
# Doctest python code in docs, python code in src docstrings, test functions in tests
testpaths = "src tests"
asyncio_mode = "auto"


[tool.coverage.run]
patch = ["subprocess"]
Expand All @@ -88,8 +91,9 @@ skipsdist=True
# Don't create a virtualenv for the command, requires tox-direct plugin
direct = True
passenv = *
allowlist_externals =
allowlist_externals =
pytest
pytest-asyncio
pre-commit
mypy
sphinx-build
Expand Down
30 changes: 16 additions & 14 deletions src/pytac/cothread_cs.py → src/pytac/aioca_cs.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import logging

from cothread.catools import ca_nothing, caget, caput
from aioca import CANothing, caget, caput

from pytac.cs import ControlSystem
from pytac.exceptions import ControlSystemException


class CothreadControlSystem(ControlSystem):
"""A control system using cothread to communicate with EPICS.
class AIOCAControlSystem(ControlSystem):
"""A control system using aioca to communicate with EPICS.

N.B. this is the default control system. It is used to communicate over
channel access with the hardware in the ring.
Expand All @@ -19,7 +19,7 @@ def __init__(self, timeout=1.0, wait=False):
self._timeout = timeout
self._wait = wait

def get_single(self, pv, throw=True):
async def get_single(self, pv, throw=True):
"""Get the value of a given PV.

Args:
Expand All @@ -35,16 +35,16 @@ def get_single(self, pv, throw=True):
ControlSystemException: if it cannot connect to the specified PV.
"""
try:
return caget(pv, timeout=self._timeout, throw=True)
except ca_nothing:
return await caget(pv, timeout=self._timeout, throw=True)
except CANothing:
error_msg = f"Cannot connect to {pv}."
if throw:
raise ControlSystemException(error_msg) # noqa: B904
else:
logging.warning(error_msg)
return None

def get_multiple(self, pvs, throw=True):
async def get_multiple(self, pvs, throw=True):
"""Get the value for given PVs.

Args:
Expand All @@ -59,11 +59,11 @@ def get_multiple(self, pvs, throw=True):
Raises:
ControlSystemException: if it cannot connect to one or more PVs.
"""
results = caget(pvs, timeout=self._timeout, throw=False)
results = await caget(pvs, timeout=self._timeout, throw=False)
return_values = []
failures = []
for result in results:
if isinstance(result, ca_nothing):
if isinstance(result, CANothing):
logging.warning(f"Cannot connect to {result.name}.")
if throw:
failures.append(result)
Expand All @@ -75,7 +75,7 @@ def get_multiple(self, pvs, throw=True):
raise ControlSystemException(f"{len(failures)} caget calls failed.")
return return_values

def set_single(self, pv, value, throw=True):
async def set_single(self, pv, value, throw=True):
"""Set the value of a given PV.

Args:
Expand All @@ -91,17 +91,17 @@ def set_single(self, pv, value, throw=True):
ControlSystemException: if it cannot connect to the specified PV.
"""
try:
caput(pv, value, timeout=self._timeout, throw=True, wait=self._wait)
await caput(pv, value, timeout=self._timeout, throw=True, wait=self._wait)
return True
except ca_nothing:
except CANothing:
error_msg = f"Cannot connect to {pv}."
if throw:
raise ControlSystemException(error_msg) # noqa: B904
else:
logging.warning(error_msg)
return False

def set_multiple(self, pvs, values, throw=True):
async def set_multiple(self, pvs, values, throw=True):
"""Set the values for given PVs.

Args:
Expand All @@ -122,7 +122,9 @@ def set_multiple(self, pvs, values, throw=True):
"""
if len(pvs) != len(values):
raise ValueError("Please enter the same number of values as PVs.")
status = caput(pvs, values, timeout=self._timeout, throw=False, wait=self._wait)
status = await caput(
pvs, values, timeout=self._timeout, throw=False, wait=self._wait
)
return_values = []
failures = []
for stat in status:
Expand Down
34 changes: 25 additions & 9 deletions src/pytac/data_source.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Module containing pytac data source classes."""

import inspect

import pytac
from pytac.exceptions import DataSourceException, FieldException

Expand Down Expand Up @@ -189,7 +191,7 @@ def set_unitconv(self, field, uc):
"""
self._uc[field] = uc

def get_value(
async def get_value(
self,
field: str,
handle: str = pytac.RB,
Expand Down Expand Up @@ -225,12 +227,12 @@ def get_value(
if data_source_type == pytac.DEFAULT:
data_source_type = self.default_data_source
data_source = self.get_data_source(data_source_type)
value = data_source.get_value(field, handle, throw)
value = await data_source.get_value(field, handle, throw)
return self.get_unitconv(field).convert(
value, origin=data_source.units, target=units
)

def set_value(
async def set_value(
self,
field: str,
value: float,
Expand Down Expand Up @@ -264,7 +266,7 @@ def set_value(
value = self.get_unitconv(field).convert(
value, origin=units, target=data_source.units
)
data_source.set_value(field, value, throw)
await data_source.set_value(field, value, throw)


class DeviceDataSource(DataSource):
Expand Down Expand Up @@ -321,7 +323,7 @@ def get_fields(self):
"""
return self._devices.keys()

def get_value(self, field, handle, throw=True):
async def get_value(self, field, handle, throw=True):
"""Get the value of a readback or setpoint PV for a field from the
data_source.

Expand All @@ -337,9 +339,17 @@ def get_value(self, field, handle, throw=True):
Raises:
FieldException: if the device does not have the specified field.
"""
return self.get_device(field).get_value(handle, throw)

def set_value(self, field, value, throw=True):
device = self.get_device(field)
# TODO some devices dont need to be awaited as they are just retrieving stored
# data, but others get data from PVs so do, make this better
val = 0
if inspect.iscoroutinefunction(device.get_value):
val = await device.get_value(handle, throw)
else:
val = device.get_value(handle, throw)
return val
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs discussion on how to handle async vs non async devices.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to get an elements value, currently we must await it as it is possible that we need to do an async operation to get the data:

await element.get_value()

This then has to do a get_value on its data source manager, which again may have to do async work so must be awaited:

await data_source_manager.get_value()

This function then gets the data source which is non async, then it calls get_value on the datasource which must be awaited as the datasource could do async work:

await data_source.get_value()

Two examples of data sources are ATLatticeDataSource and SimpleDevice. Both are used by Virtac, when getting a value from an ATLatticeDataSource we must await a recalculation. When using SimpleDevice we are just getting stored data, so we dont need to do an await.

Currently these two cases are handled with: if inspect.iscoroutinefunction(device.get_value).
Maybe this could be improved?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it an issue that we must await when getting a value even though it doesnt actually do any async actions? It will have a very slight computation overhead.

Most of our use cases are largely async. The main situation where we might not be is if someone loads an AT lattice and just wants to get the default values from it using pytac. They would have to await everything even though it is just reading data from the static lattice.


async def set_value(self, field, value, throw=True):
"""Set the value of a readback or setpoint PV for a field from the
data_source.

Expand All @@ -352,4 +362,10 @@ def set_value(self, field, value, throw=True):
Raises:
FieldException: if the device does not have the specified field.
"""
self.get_device(field).set_value(value, throw)
device = self.get_device(field)
# TODO some devices dont need to be awaited as they are just setting local
# data, but others set data to PVs, so do, make this better
if inspect.iscoroutinefunction(device.set_value):
await device.set_value(value, throw)
else:
device.set_value(value, throw)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

17 changes: 7 additions & 10 deletions src/pytac/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def is_enabled(self):
"""
return bool(self._enabled)

def get_value(self, handle, throw=True):
async def get_value(self, handle, throw=True):
"""Read the value of a readback or setpoint PV.

Args:
Expand All @@ -182,9 +182,9 @@ def get_value(self, handle, throw=True):
Raises:
HandleException: if the requested PV doesn't exist.
"""
return self._cs.get_single(self.get_pv_name(handle), throw)
return await self._cs.get_single(self.get_pv_name(handle), throw)

def set_value(self, value, throw=True):
async def set_value(self, value, throw=True):
"""Set the device value.

Args:
Expand All @@ -195,7 +195,7 @@ def set_value(self, value, throw=True):
Raises:
HandleException: if no setpoint PV exists.
"""
self._cs.set_single(self.get_pv_name(pytac.SP), value, throw)
return await self._cs.set_single(self.get_pv_name(pytac.SP), value, throw)

def get_pv_name(self, handle):
"""Get the PV name for the specified handle.
Expand All @@ -220,9 +220,6 @@ def get_pv_name(self, handle):
class PvEnabler:
"""A PvEnabler class to check whether a device is enabled.

The class will behave like True if the PV value equals enabled_value,
and False otherwise.

.. Private Attributes:
_pv (str): The PV name.
_enabled_value (str): The value for PV for which the device should
Expand All @@ -244,11 +241,11 @@ def __init__(self, pv, enabled_value, cs):
self._enabled_value = str(int(float(enabled_value)))
self._cs = cs

def __bool__(self):
async def is_enabled(self):
"""Used to override the 'if object' clause.

Returns:
bool: True if the device should be considered enabled.
"""
pv_value = self._cs.get_single(self._pv)
return self._enabled_value == str(int(float(pv_value)))
pv_value = await self._cs.get_single(self._pv)
return self._enabled_value == str(int(float(pv_value))) # ???
10 changes: 6 additions & 4 deletions src/pytac/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ def is_in_family(self, family):
"""
return family.lower() in self._families

def get_value(
async def get_value(
self,
field,
handle=pytac.RB,
Expand Down Expand Up @@ -257,15 +257,15 @@ def get_value(
FieldException: if the element does not have the specified field.
"""
try:
return self._data_source_manager.get_value(
return await self._data_source_manager.get_value(
field, handle, units, data_source, throw
)
except DataSourceException as e:
raise DataSourceException(f"{self}: {e}") from e
except FieldException as e:
raise FieldException(f"{self}: {e}") from e

def set_value(
async def set_value(
self,
field,
value,
Expand All @@ -290,7 +290,9 @@ def set_value(
FieldException: if the element does not have the specified field.
"""
try:
self._data_source_manager.set_value(field, value, units, data_source, throw)
await self._data_source_manager.set_value(
field, value, units, data_source, throw
)
except DataSourceException as e:
raise DataSourceException(f"{self}: {e}") from e
except FieldException as e:
Expand Down
Loading