Empty spectrum and image attributes

Warning

This is the most significant API change. It could cause errors in existing Tango clients and devices.

Both server-side writing, and client-side reading could be affected. There are differences depending on an attribute’s data type and its access type (read/read-write). We go into detail below. The goal of the changes was to make usage more intuitive, and to provide a more consistent API.

Writing - server side

When an empty sequence is written to a spectrum or image attribute of type DevString the write function used to receive a None value, but now it will receive an empty list. For other types, the behaviour is unchanged - they were already receiving an empty numpy.ndarray.

Example device:

from tango import AttrWriteType
from tango.server import Device, attribute

class Test(Device):
    str1d = attribute(dtype=(str,), max_dim_x=4, access=AttrWriteType.WRITE)
    int1d = attribute(dtype=(int,), max_dim_x=4, access=AttrWriteType.WRITE)
    str2d = attribute(dtype=((str,),), max_dim_x=4, max_dim_y=4, access=AttrWriteType.WRITE)
    int2d = attribute(dtype=((int,),), max_dim_x=4, max_dim_y=4, access=AttrWriteType.WRITE)

    def write_str1d(self, values):
        print(f"Writing str1d: values={values!r}, type={type(values)}")

    def write_int1d(self, values):
        print(f"Writing int1d: values={values!r}, type={type(values)}")

    def write_str2d(self, values):
        print(f"Writing str2d: values={values!r}, type={type(values)}")

    def write_int2d(self, values):
        print(f"Writing int2d: values={values!r}, type={type(values)}")

If a client writes empty data to the device:

>>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")
>>> dp.str1d = []
>>> dp.int1d = []
>>> dp.str2d = [[]]
>>> dp.int2d = [[]]

Old: The output from a v9.3.x PyTango device would be:

Writing str1d: values=None, type=<class 'NoneType'>
Writing int1d: values=array([], dtype=int64), type=<class 'numpy.ndarray'>
Writing str2d: values=None, type=<class 'NoneType'>
Writing int2d: values=array([], shape=(1, 0), dtype=int64), type=<class 'numpy.ndarray'>

New: The output from a v9.4.x PyTango device would be:

Writing str1d: values=[], type=<class 'list'>
Writing int1d: values=array([], dtype=int64), type=<class 'numpy.ndarray'>
Writing str2d: values=[], type=<class 'list'>
Writing int2d: values=array([], shape=(1, 0), dtype=int64), type=<class 'numpy.ndarray'>

Notice the change in values received for the str1d and str2d attributes. If your existing devices have special handling for None, then they may need to change.

Reading - client side

When clients read from spectrum and image attributes with empty values, clients will now receive an empty sequence instead of a None value. For DevString and DevEnum types, the sequence is a tuple, while other types get a numpy.ndarray by default.

There is a subtle inconsistency in PyTango 9.3.x - empty read-only spectrum and image attributes always returned None values, but read-write spectrum attributes can return empty sequences instead of None values. It depends on the set point (written value) stored for the attribute - if it is non-empty, then the client gets an empty sequence. This is shown in the examples below. From PyTango 9.4.x, the behaviour is more consistent.

Warning

Reading attributes of any type can still produce a None value if the quality is ATTR_INVALID. Client-side code should always be prepared for this. This behaviour is unchanged in PyTango 9.4.x (an exception being the fix for enumerated types using the high-level API, so that they also return None).

Note

It is possible to get the data returned in other container types using the extract_as argument with tango.DeviceProxy.read_attribute().

This change affects values received via both the high-level API, and the low-level method it uses, tango.DeviceProxy.read_attribute(). It also affects all related read methods: tango.DeviceProxy.read_attributes(), tango.DeviceProxy.read_attribute_asynch(), tango.DeviceProxy.read_attribute_reply(), tango.DeviceProxy.read_attributes_asynch(), tango.DeviceProxy.read_attributes_reply().

The read attribute methods return data via tango.DeviceAttribute objects. These include a value field for the read value, and a w_value field for the last set point (i.e., last value written). Both of these fields are affected by this change.

Example device:

from enum import IntEnum
from tango import AttrWriteType
from tango.server import Device, attribute

class AnEnum(IntEnum):
   A = 0
   B = 1

class Test(Device):
    @attribute(dtype=(str,), max_dim_x=4)
    def str1d(self):
        return []

    @attribute(dtype=(AnEnum,), max_dim_x=4)
    def enum1d(self):
        return []

    @attribute(dtype=(int,), max_dim_x=4, access=AttrWriteType.READ)
    def int1d(self):
        return []

    @attribute(dtype=(int,), max_dim_x=4, access=AttrWriteType.READ_WRITE)
    def int1d_rw(self):
        return []

    @int1d_rw.write
    def int1d_rw(self, values):
        print(f"Writing int1d_rw: values={values!r}, type={type(values)}")

    @attribute(dtype=((str,),), max_dim_x=4, max_dim_y=4)
    def str2d(self):
        return [[]]

    @attribute(dtype=((int,),), max_dim_x=4, max_dim_y=4)
    def int2d(self):
        return [[]]

High-level API reads

Old: A PyTango 9.3.x client reads the empty data from the device using the high-level API:

>>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

>>> value = dp.str1d
>>> value, type(value)
(None, <class 'NoneType'>)

>>> value = dp.enum1d  # broken in PyTango 9.3.x
Traceback: ... ValueError: None is not a valid enum1d

>>> value = dp.int1d  # read-only attribute
>>> value, type(value)
(None, <class 'NoneType'>)

>>> value = dp.int1d_rw  # read-write attribute (default set point is [0])
>>> value, type(value)
(array([], dtype=int64), <class 'numpy.ndarray'>)
>>> dp.int1d_rw = []  # write empty value (set point changed to empty)
>>> value = dp.int1d_rw  # read again, after set point changed
>>> value, type(value)
(None, <class 'NoneType'>)

>>> value = dp.str2d
>>> value, type(value)
(None, <class 'NoneType'>)

>>> value = dp.int2d
>>> value, type(value)
(None, <class 'NoneType'>)

In the above example, notice that high-level API reads of enumerated spectrum types fail under PyTango 9.3.x. Also see the difference in behaviour between read-only attributes like int1d and read-write attributes like int1d_rw. Read-write spectrum attributes behave inconsistently with empty data prior to PyTango 9.4.x.

New: A PyTango 9.4.x client reads the empty data from the device using the high-level API (device server has been restarted after previous example):

>>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

>>> value = dp.str1d
>>> value, type(value)
((), <class 'tuple'>)

>>> value = dp.enum1d
>>> value, type(value)
((), <class 'tuple'>)

>>> value = dp.int1d  # read-only attribute
>>> value, type(value)
(array([], dtype=int64), <class 'numpy.ndarray'>)

>>> value = dp.int1d_rw  # read-write attribute (default set point is [0])
>>> value, type(value)
(array([], dtype=int64), <class 'numpy.ndarray'>)
>>> dp.int1d_rw = []  # write empty value (set point changed to empty)
>>> value = dp.int1d_rw  # read again, after set point changed
>>> value, type(value)
(array([], dtype=int64), <class 'numpy.ndarray'>)

>>> value = dp.str2d
>>> value, type(value)
((), <class 'tuple'>)

>>> value = dp.int2d
>>> value, type(value)
(array([], shape=(1, 0), dtype=int64), <class 'numpy.ndarray'>)

Low-level API reads

In these examples, focus on the value field (the value read back), which changes as above, and the type field. Using PyTango 9.3.x, the type is number 100, which indicates an unknown type, while with PyTango 9.4.0, the type stays correct even when the value is empty. The change in type is something updated in cppTango 9.4.1.

Old: A PyTango 9.3.x client reads the empty data from the device using the low-level API:

>>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

>>> print(dp.read_attribute("str1d"))
DeviceAttribute[
data_format = tango._tango.AttrDataFormat.SPECTRUM
      dim_x = 0
      dim_y = 0
 has_failed = False
   is_empty = True
       name = 'str1d'
    nb_read = 0
 nb_written = 0
    quality = tango._tango.AttrQuality.ATTR_VALID
r_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
       time = TimeVal(tv_nsec = 0, tv_sec = 1676068470, tv_usec = 650091)
       type = tango._tango.CmdArgType(100)
      value = None
    w_dim_x = 0
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
    w_value = None]

>>> print(dp.read_attribute("int1d"))
DeviceAttribute[
data_format = tango._tango.AttrDataFormat.SPECTRUM
      dim_x = 0
      dim_y = 0
 has_failed = False
   is_empty = False
       name = 'int1d'
    nb_read = 0
 nb_written = 1
    quality = tango._tango.AttrQuality.ATTR_VALID
r_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
       time = TimeVal(tv_nsec = 0, tv_sec = 1676068478, tv_usec = 597279)
       type = tango._tango.CmdArgType.DevLong64
      value = array([], dtype=int64)
    w_dim_x = 1
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 1, dim_y = 0)
    w_value = array([0])]

>>> print(dp.read_attribute("str2d"))
DeviceAttribute[
data_format = tango._tango.AttrDataFormat.IMAGE
      dim_x = 0
      dim_y = 1
 has_failed = False
   is_empty = True
       name = 'str2d'
    nb_read = 0
 nb_written = 0
    quality = tango._tango.AttrQuality.ATTR_VALID
r_dimension = AttributeDimension(dim_x = 0, dim_y = 1)
       time = TimeVal(tv_nsec = 0, tv_sec = 1676068484, tv_usec = 896408)
       type = tango._tango.CmdArgType(100)
      value = None
    w_dim_x = 0
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
    w_value = None]

>>> print(dp.read_attribute("int2d"))
DeviceAttribute[
data_format = tango._tango.AttrDataFormat.IMAGE
      dim_x = 0
      dim_y = 1
 has_failed = False
   is_empty = True
       name = 'int2d'
    nb_read = 0
 nb_written = 0
    quality = tango._tango.AttrQuality.ATTR_VALID
r_dimension = AttributeDimension(dim_x = 0, dim_y = 1)
       time = TimeVal(tv_nsec = 0, tv_sec = 1676068489, tv_usec = 330193)
       type = tango._tango.CmdArgType(100)
      value = None
    w_dim_x = 0
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
    w_value = None]

New: A PyTango 9.4.x client reads the empty data from the device using the low-level API:

>>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

>>> print(dp.read_attribute("str1d"))
DeviceAttribute[
data_format = tango._tango.AttrDataFormat.SPECTRUM
      dim_x = 0
      dim_y = 0
 has_failed = False
   is_empty = True
       name = 'str1d'
    nb_read = 0
 nb_written = 0
    quality = tango._tango.AttrQuality.ATTR_VALID
r_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
       time = TimeVal(tv_nsec = 0, tv_sec = 1676068550, tv_usec = 333749)
       type = tango._tango.CmdArgType.DevString
      value = ()
    w_dim_x = 0
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
    w_value = ()]

>>> print(dp.read_attribute("int1d"))
DeviceAttribute[
data_format = tango._tango.AttrDataFormat.SPECTRUM
      dim_x = 0
      dim_y = 0
 has_failed = False
   is_empty = False
       name = 'int1d'
    nb_read = 0
 nb_written = 1
    quality = tango._tango.AttrQuality.ATTR_VALID
r_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
       time = TimeVal(tv_nsec = 0, tv_sec = 1676068554, tv_usec = 243413)
       type = tango._tango.CmdArgType.DevLong64
      value = array([], dtype=int64)
    w_dim_x = 1
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 1, dim_y = 0)
    w_value = array([0])]

>>> print(dp.read_attribute("str2d"))
DeviceAttribute[
data_format = tango._tango.AttrDataFormat.IMAGE
      dim_x = 0
      dim_y = 1
 has_failed = False
   is_empty = True
       name = 'str2d'
    nb_read = 0
 nb_written = 0
    quality = tango._tango.AttrQuality.ATTR_VALID
r_dimension = AttributeDimension(dim_x = 0, dim_y = 1)
       time = TimeVal(tv_nsec = 0, tv_sec = 1676068558, tv_usec = 191433)
       type = tango._tango.CmdArgType.DevString
      value = ()
    w_dim_x = 0
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
    w_value = ()]

>>> print(dp.read_attribute("int2d"))
DeviceAttribute[
data_format = tango._tango.AttrDataFormat.IMAGE
      dim_x = 0
      dim_y = 1
 has_failed = False
   is_empty = True
       name = 'int2d'
    nb_read = 0
 nb_written = 0
    quality = tango._tango.AttrQuality.ATTR_VALID
r_dimension = AttributeDimension(dim_x = 0, dim_y = 1)
       time = TimeVal(tv_nsec = 0, tv_sec = 1676068562, tv_usec = 50107)
       type = tango._tango.CmdArgType.DevLong64
      value = array([], shape=(1, 0), dtype=int64)
    w_dim_x = 0
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
    w_value = array([], shape=(0, 0), dtype=int64)]

Low-level API reads of set point (write value)

In these examples, focus on the w_value field which is the set point, or last written value.

Old: A PyTango 9.3.x client changes the set point and reads using the low-level API:

>>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

>>> dp.int1d_rw = [1, 2]
>>> print(dp.read_attribute("int1d_rw"))
DeviceAttribute[
    ...
       type = tango._tango.CmdArgType.DevLong64
    w_dim_x = 2
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 2, dim_y = 0)
    w_value = array([1, 2])]

>>> dp.int1d_rw = []
>>> print(dp.read_attribute("int1d_rw"))
DeviceAttribute[
    ...
       type = tango._tango.CmdArgType(100)
    w_dim_x = 0
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
    w_value = None]

New: A PyTango 9.4.x client changes the set point and reads using the low-level API:

>>> dp = tango.DeviceProxy("tango://127.0.0.1:8888/test/nodb/test#dbase=no")

>>> dp.int1d_rw = [1, 2]
>>> print(dp.read_attribute("int1d_rw"))
DeviceAttribute[
    ...
       type = tango._tango.CmdArgType.DevLong64
    w_dim_x = 2
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 2, dim_y = 0)
    w_value = array([1, 2])]

>>> dp.int1d_rw = []
>>> print(dp.read_attribute("int1d_rw"))
DeviceAttribute[
    ...
       type = tango._tango.CmdArgType.DevLong64
    w_dim_x = 0
    w_dim_y = 0
w_dimension = AttributeDimension(dim_x = 0, dim_y = 0)
    w_value = array([], dtype=int64)]