diff --git a/ophyd/signal.py b/ophyd/signal.py index 36fbcc9dc..9e82712b6 100644 --- a/ophyd/signal.py +++ b/ophyd/signal.py @@ -44,18 +44,10 @@ DEFAULT_EPICSSIGNAL_VALUE = UNSET_VALUE -class ReadTimeoutError(TimeoutError): - ... +class ReadTimeoutError(TimeoutError): ... -class ConnectionTimeoutError(TimeoutError): - ... - - -class UndeterminedDataType(Exception): - def __init__(self, dtype=None, shape=None): - self.dtype = np.dtype(dtype).name if dtype is not None else "" - self.shape = shape +class ConnectionTimeoutError(TimeoutError): ... class Signal(OphydObject): @@ -104,7 +96,7 @@ def __init__( self, *, name, - value=UNSET_VALUE, + value=0.0, dtype=None, shape=None, timestamp=None, @@ -128,37 +120,48 @@ def __init__( self._dispatcher = cl.get_dispatcher() self._metadata_thread_ctx = self._dispatcher.get_thread_context("monitor") - # try to apply good default, if possible - try: - if dtype is None: - if value is UNSET_VALUE: - if shape is None: - value = 0.0 - raise UndeterminedDataType() - else: - raise UndeterminedDataType(shape=shape) + self._value_dtype_str = ( + "" if dtype in (None, "string") else np.dtype(dtype).name + ) + self._value_shape = shape if not dtype == "string" else () + # check if value corresponds to specified info (or if it is compatible) + if dtype not in (None, "string") and value is not UNSET_VALUE: + value_array = np.asanyarray(value) + + try: + value_array.astype(dtype, casting="same_kind") + except TypeError: + dtype_ok = False + # check if conversion between signed int to unsigned int would be fine + if value_array.dtype.kind == "i" and np.dtype(dtype).kind == "u": + bounds = np.iinfo(dtype) + if np.all(value_array >= bounds.min) and np.all( + value_array <= bounds.max + ): + dtype_ok = True + else: + dtype_ok = True + + if not dtype_ok: + if np.isscalar(value) and value == 0.0: + # default is not ok, change to UNSET_VALUE + # (value will be read via .get() - it does not mean it will correspond + # to the desired dtype, though...) + value = UNSET_VALUE else: - try: - inferred_type = data_type(value) - except ValueError: - raise UndeterminedDataType() - else: - if inferred_type == "integer": - dtype = int - elif inferred_type == "number": - dtype = float - else: - raise UndeterminedDataType() - if shape is None: - shape = data_shape(value) - except UndeterminedDataType as exc: - self._value_dtype_str = exc.dtype - self._value_shape = exc.shape - self._readback = value + raise TypeError( + f"The value {value} does not match the required dtype {dtype}." + ) else: - self._value_dtype_str = "" if dtype == "string" else np.dtype(dtype).name - self._value_shape = () if shape is None else shape - self._readback = value + if dtype == "string" and np.isscalar(value) and value == 0.0: + value = UNSET_VALUE + if shape is not None and value is not UNSET_VALUE: + if np.asanyarray(value).shape != shape: + raise TypeError( + f"The value {value} does not have the required shape {shape}." + ) + + self._readback = value if timestamp is None: timestamp = time.time() @@ -1018,6 +1021,8 @@ def __init__( if string: kwargs["dtype"] = "string" + kwargs.setdefault("value", UNSET_VALUE) # no default + super().__init__(name=name, metadata=metadata, **kwargs) validate_pv_name(read_pv) diff --git a/ophyd/tests/test_signal.py b/ophyd/tests/test_signal.py index 37278388d..470d5fcce 100644 --- a/ophyd/tests/test_signal.py +++ b/ophyd/tests/test_signal.py @@ -721,7 +721,7 @@ def test_signal_dtype_shape_info(fake_motor_ioc, cleanup): original = Signal(name="original", value=1) original_desc = original.describe()["original"] assert original_desc["dtype"] == "integer" - assert original_desc["dtype_numpy"] == "int64" + assert original_desc["dtype_numpy"] == "" assert original_desc["shape"] == [] assert original.get() == 1 original = Signal(name="original", value="On")