Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

signal: init value can be a ValueInfo structure with dtype, shape and… #1194

Merged
merged 4 commits into from
Nov 12, 2024

Conversation

mguijarr
Copy link

… default value to characterize the value

Solves issue #1193 (and #1178 ?)

@mguijarr
Copy link
Author

Please comment , if you agree with the proposed change I will add a test.

ophyd/signal.py Outdated
@@ -95,7 +102,7 @@ def __init__(
self,
*,
name,
value=0.0,
value=DEFAULT_EPICSSIGNAL_VALUE,
Copy link
Contributor

Choose a reason for hiding this comment

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

If we move this here I suspect we will have to hoist the logic to handle this from EpicsSignal up to the base class.

Copy link
Author

Choose a reason for hiding this comment

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

well, it looks like DEFAULT_EPICSSIGNAL_VALUE is more used as a sentinel value to indicate if a default has been provided or not - it is currently already used in Signal class, see line 475:

if self._readback is DEFAULT_EPICSSIGNAL_VALUE:
            val = self.get()

or derived classes which do not have an explicit relation with EPICS (i.e not deriving from EpicsSignalBase).

I propose to rename it, then ? Like UNSET_VALUE or UNSPECIFIED_VALUE ?

It seems None is considered as a potential ok value. Is it the case ? Otherwise we could initialize with None.

In describe(), I think the key point is to really read the value if it is not specified. Any way to do it like this is fine for me.

What do you think ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, if we are already using it is the base class than I'm 50/50 on adding an alias (the hassle of anyone downstream using the old name needing to update is not worth it to remove the old name) vs just using it.

The concern I still have is that in Signal.get we just return self._readback which means that there is a chance of returning DEFAULT_EPICSSIGNAL_VALUE to the user (which we definitely should not do). The minimal fix is probably to raise in Signal.get if the value has never been set.

Copy link
Author

@mguijarr mguijarr Jun 4, 2024

Choose a reason for hiding this comment

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

Wouldn't it be better to initialize to None and to return None if it has not been set? I am very new to Ophyd, so I do not know the logic/philosophy behind reading signals and why DEFAULT_EPICSSIGNAL_VALUE was introduced at first place

Copy link
Author

Choose a reason for hiding this comment

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

Otherwise I can make an alias and raise in get() as you suggest, no problem 👍🏻

Tell me what you prefer

Copy link
Contributor

Choose a reason for hiding this comment

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

tl;dr: I would prefer the alias + raise in get path as the minimal change.

pyepics (the default wrapping we use to talk channel access over in Python) returns None when it fails so defaulting to None means we could not tell the difference between "failed to read" vs "never read". The logic was to have a unique sentinel that would never come to use from pyepics and would never be passed to us by the user so we could tell if we had ever been told a value for this signal. Basically detect the start up transient and allows lazy connection (each PV is its own connection dance with a server and if we never read it then it is better to never try to connect).

Going with a custom default object also helps makes clear that it is "default but invalid" where as None is frequently "default and valid" or "placeholder to tell the system to do the default thing" and I think that is an important distinction here.

I'm a bit fuzzy remember choices we made probably 8 years ago now, but I am biased to trusted my previous self 🤣

@tacaswell
Copy link
Contributor

Interesting idea!

I have some concerns that this will box us out from ever fixing EpicsSignal to trust the metadata that comes across on the channel rather than infering the dtype from the values, but I think that if the rules go 1. user passed 2. Channel provided 3. inferred would be ok.

We have been using dtype_str for providing higher-fidelitiy data type information (see discussion in bluesky/event-model#215 despite not getting that PR merged, we have started to use it in-practice in tiled) so we probably should use a different name internally.

I lean +.75 on this being a good idea .

@mguijarr
Copy link
Author

mguijarr commented Jun 4, 2024

I have some concerns that this will box us out from ever fixing EpicsSignal to trust the metadata that comes across on the channel rather than infering the dtype from the values, but I think that if the rules go 1. user passed 2. Channel provided 3. inferred would be ok.

I agree with this.

We have been using dtype_str for providing higher-fidelitiy data type information (see discussion in bluesky/event-model#215 despite not getting that PR merged, we have started to use it in-practice in tiled) so we probably should use a different name internally.

Ok I renamed shape and dtype vars to _value_shape and _value_dtype_str.

@prjemian
Copy link
Contributor

prjemian commented Jun 4, 2024

The value of DEFAULT_EPICSSIGNAL_VALUE should be set such that (lazy) programmatic use does not rely on the actual value of the default. Random value should be assigned to require use of the symbol, such as one of these:

DEFAULT_EPICSSIGNAL_VALUE = object()
DEFAULT_EPICSSIGNAL_VALUE = uuid.uuid4()

@mguijarr mguijarr force-pushed the signal_valueinfo branch 10 times, most recently from 6c7a836 to c2be5d3 Compare June 5, 2024 14:31
@mguijarr
Copy link
Author

mguijarr commented Jun 5, 2024

Hi @tacaswell , @prjemian 😄 This is ready for me. I don't know why doc would not build... ?

I added specific tests for the feature. In addition to what we discussed above, I renamed ValueInfo to NumericValueInfo because it is not adapted to strings
for example. I added source_name as a property, in order to make describe() more generic. I made that shape in describe() is a tuple rather than sometimes a list, sometimes a tuple (because, for numpy arrays, it is a tuple normally).

I would be happy to work on having EpicsSignalBase trying to specify dtype and shape from metadata but I have no idea how to do it since I am very new to EPICS, PyEpics in general. Normally it is easy to do it like @tacaswell proposes, 1. user defined , 2. EPICS metadata or 3. inference if there is nothing else... Maybe in another MR ?

@mguijarr
Copy link
Author

No news ; is there anything wrong with the proposal?

@mguijarr
Copy link
Author

mguijarr commented Jul 2, 2024

@prjemian @tacaswell I apologize in advance if you're on holidays or too busy. I really need to have proper type info in Ophyd, sorry to insist - if you think this proposal can be improved do not hesitate to tell me, or if I have to do something to make it approved ?

@prjemian
Copy link
Contributor

prjemian commented Jul 2, 2024

I'm OK with the part on which I commented. I'll leave it to Tom for the PR review.

@mguijarr mguijarr marked this pull request as draft July 9, 2024 09:09
@wakonig
Copy link

wakonig commented Oct 11, 2024

Hi @tacaswell! What's the status on this one? It would help us a lot if we could get this one merged soon. Thanks!

@mguijarr
Copy link
Author

Hi again... I am reviving this discussion, I really need the added functionality so can we find an agreement on this?

@tacaswell
Copy link
Contributor

Sorry I never found you again at NOBUGs!

ophyd/signal.py Outdated Show resolved Hide resolved
ophyd/signal.py Outdated
@@ -95,7 +104,7 @@ def __init__(
self,
*,
name,
value=0.0,
value=NumericValueInfo(float, (), 0.0),
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than merging the expected info and the default value, can we add a new key(s) of expected_dtype_and_shape or dtype and shape? In __init__ it seems reasonable to do a cast, but in describe I wonder if we should be checking if the value we have in had is actually the correct dtype/shape or not?

Copy link
Author

Choose a reason for hiding this comment

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

In my original proposal I didn't want to add more keyword args, but yeah sure - I introduced dtype and shape in latest version.

For the check in describe: I didn't change anything yet


original = Signal(name="original")
original_desc = original.describe()["original"]
assert original_desc["dtype"] == "float64"
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

Ok so newest version has dtype and dtype_numpy

@tacaswell
Copy link
Contributor

The more-complete dtype information should be put in dtype_numpy. This going in everywhere at the bottom would probably be a big help (as we have been stuffing it in ad-hoc at NSLS-II).

https://github.com/bluesky/event-model/blob/a1fdaec0c45b89e513d0be9dba0bfc453982e5bb/src/event_model/schemas/event_descriptor.json#L63-L91

@mguijarr mguijarr force-pushed the signal_valueinfo branch 11 times, most recently from 75c4f6e to 7e33cc1 Compare November 4, 2024 14:08
@mguijarr
Copy link
Author

mguijarr commented Nov 4, 2024

@tacaswell sorry for the delay... I struggled a bit with the changes (this is why test didn't pass last time).

In fact I asked me a lot of questions, finally I think I get it all right.

For the review: maybe the easiest is to have a look to the new test (test_signal_dtype_shape_info), because it shows the expected behavior. So if you agree with this, then you can maybe look at changes in details ?

What I find relevant to know:

  • I added keyword arguments dtype and shape which defaults to None
  • The default value is 0.0, but I identify it with a DefaultFloat class
    • this is to distinguish if user provided a value, or left default
    • indeed, I need to know this in some circumstances like a string signal without specific default passed by the user
      • in this case, I do not initialize with 0.0 but with UNSET_VALUE (previously DEFAULT_EPICSSIGNAL_VALUE, which is still there for compatibility)
  • the UNSET_VALUE has a __repr__, so we can see it while debugging more easily
  • if a default value is passed, there is a check to validate if it corresponds to the dtype (or shape)
    • it was not so obvious to allow numpy.int64 (default for ints) to an unsigned int for example, numpy can be tricky!
    • there is no cast, but TypeError is raised if it does not correspond
  • I removed a weird __copy__ dunder
    • I could not see why it was like this, no specific test neither
    • with the original __copy__ one test was not passing, because it recreates the object with different constructor arguments
  • in describe(), if the dtype and shape are specified it is used for determining infos instead of inferring from data
    • otherwise it is like before
  • in describe(), if numpy dtype is specified it goes to dtype_numpy as you required
    • however, if it is not available the key is also not set in the dictionary (allows old tests to pass)
  • I added .source_name as a property, in order to make describe() more generic
  • I changed data_shape to return a tuple (because, it is really a shape), however it is returned as a list by describe() (like now) to make it JSON compliant

@mguijarr
Copy link
Author

mguijarr commented Nov 4, 2024

Other remark:

  • would it be possible to get rid of integer for simple dtype in describe() ?

    • now we have dtype_numpy I don't see why we would need this
    • it would simplify some code
  • would it make more sense to not initialize value to 0.0 ?

    • I would prefer UNSET_VALUE

@tacaswell
Copy link
Contributor

would it be possible to get rid of integer for simple dtype in describe() ?

Maybe we could get the RE to look at numpy_dtype and infer the correct dtype, however in the document model dtype is required but numpy_dytpe is optional (https://github.com/bluesky/event-model/blob/a1fdaec0c45b89e513d0be9dba0bfc453982e5bb/src/event_model/schemas/event_descriptor.json#L145-L149) so I think it makes sense to always provide both from ophyd.

would it make more sense to not initialize value to 0.0 ?

That seems fine as 0.0 is not a valid value for a great many dtypes!

@@ -588,14 +588,14 @@ def _repr_info(self):
if self._parent is not None:
yield ("parent", self.parent.name)

def __copy__(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you remove the custom __copy__?

Copy link
Author

Choose a reason for hiding this comment

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

I said in the comment above:

  • I removed a weird copy dunder
    • with the original copy one test was not passing, because it recreates the object with different constructor arguments
    • I could not see why it was like this, no specific test neither

But, with my newest code the failing test works again. So I re-enabled __copy__, no prob.

(however, I still don't understand what it is for)

ophyd/signal.py Outdated Show resolved Hide resolved
@mguijarr
Copy link
Author

mguijarr commented Nov 5, 2024

@tacaswell Here we are. Should be ready to merge now ?

@mguijarr
Copy link
Author

mguijarr commented Nov 5, 2024

I think it makes sense to always provide both from ophyd.

Just to be precise: current code I sent, do not always provide dtype_numpy ; indeed, the document model check do not understand empty string as a valid dtype_numpy. So, in this case (if dtype_numpy is unknown), it is not in the dict returned by describe() at all (otherwise the verification logic has to be changed)

About replacing 0.0 default value with UNSET_VALUE:

That seems fine as 0.0 is not a valid value for a great many dtypes!

Ok. I will make another MR, then. It is not urgent for me.

@tacaswell tacaswell marked this pull request as ready for review November 12, 2024 16:57
@tacaswell tacaswell merged commit ee7ab9b into bluesky:master Nov 12, 2024
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants