A use-case I've come across a number of times now is wanting to access the results of a fragment from within a kernel.
This often comes up in the context of AggregateExpFragment where one aggregates multiple ExpFragments into a parent ExpFragment (with the notion of "a fragment is essentially a function" this is basically saying that when one has functions which call other functions, it's often useful to be able to act on their return values). One example of this is "I have a sideband thermometry AggregateExpFragment which calls a red and blue sideband and now I want to calculate nbar based on the measured populations. Another example might be I want to run a servo with a wide capture range and then use a result to seed a servo with an narrow capture range (forward a result channel to a parameter).
Since result channel sinks should never be accessed by kernel code, there is currently no sanctioned way of achieving this within ndscan. This thread is a request for comments to discuss a proposed implementation. I think a good implementation of this should:
Be ergonomic and user-friendly
Work naturally with the current limitations of ARTIQ python
Not rely on synchronous RPCs - getting results should be supported without having to leave the kernel
@dnadlinger LMK if you would be happy merging a PR along the lines of the below (and feel free to suggest names / API) and I'd be happy to write one.
Proposal 1: Introduce an API for absorbing result channels
Absorbing result channels - taking the result channel out of the scan schema seen by the ScanRunner and then re-exporting - is already used by subscans. We've discussed having a convenient API for this. I think it would look like the following:
1 - We introduce a new AbsorbedResultSink.
class AbsorbedResultSink(ResultSink):
""" Sink used when result channels are "absorbed" by another fragment.
This sink acts much like a :class:`LastValueSink` with the addition of forwarding all
pushed values to the specified sink.
"""
def __init__(self, channel: ResultChannel):
self.channel = channel
self.value = None
def push(self, value: Any) -> None:
self.channel.push(value)
self.value = value
def get_last(self) -> Any:
"""Return the last-pushed value, or ``None`` if none yet."""
return self.value
2 - We introduce a new method Fragment.absorb_result(self, channel: ResultChannel) -> AbsorbedResultSink
The returned object can then be accessed from within kernels to get the most recent value through the get_last method.
The main issue I see with a proposal along these lines is that the sink mechanism relies on synchronous RPCs. This is a shame but is essentially forced on us by limitations of ARTIQ python - the RPCs are essentially a means of allowing result channels of the same type to have different types of sinks within a single kernel. We may want to implement this (or something like it) anyway to make things like the subscan implementation a little cleaner.
Proposal 2: Introduce a new NumericChannel.get_last()
The only way I see of implementing this while avoiding sync RPCs would be to add a new NumericChannel.get_last() method.
Comments:
feel free to suggest a better name!
due to the lack of dynamic memory allocation and incomplete lifetime tracking in ARTIQ python we can't really do this for anything other than a numeric (int/float) channel. However, I suspect that will catch the vast majority of the use cases and for everything else falling back to something along the lines of the above.
It's a bit disappointing to need to add this on top of the existing sink interface, but arguably less so than having to go via the host to pass results between different fragments executing inside the same kernel!
@dnadlinger : any objections to me adding a NumericChannel.get_lastI() method as a solution to this? If you're happy with that approach, I'll submit a PR in the next few days
A use-case I've come across a number of times now is wanting to access the results of a fragment from within a kernel.
This often comes up in the context of
AggregateExpFragment
where one aggregates multipleExpFragment
s into a parentExpFragment
(with the notion of "a fragment is essentially a function" this is basically saying that when one has functions which call other functions, it's often useful to be able to act on their return values). One example of this is "I have a sideband thermometryAggregateExpFragment
which calls a red and blue sideband and now I want to calculate nbar based on the measured populations. Another example might be I want to run a servo with a wide capture range and then use a result to seed a servo with an narrow capture range (forward a result channel to a parameter).Since result channel sinks should never be accessed by kernel code, there is currently no sanctioned way of achieving this within ndscan. This thread is a request for comments to discuss a proposed implementation. I think a good implementation of this should:
@dnadlinger LMK if you would be happy merging a PR along the lines of the below (and feel free to suggest names / API) and I'd be happy to write one.
Proposal 1: Introduce an API for absorbing result channels
Absorbing result channels - taking the result channel out of the scan schema seen by the
ScanRunner
and then re-exporting - is already used by subscans. We've discussed having a convenient API for this. I think it would look like the following:1 - We introduce a new
AbsorbedResultSink
.2 - We introduce a new method
Fragment.absorb_result(self, channel: ResultChannel) -> AbsorbedResultSink
This boils down to essentially:
The returned object can then be accessed from within kernels to get the most recent value through the
get_last
method.The main issue I see with a proposal along these lines is that the sink mechanism relies on synchronous RPCs. This is a shame but is essentially forced on us by limitations of ARTIQ python - the RPCs are essentially a means of allowing result channels of the same type to have different types of sinks within a single kernel. We may want to implement this (or something like it) anyway to make things like the subscan implementation a little cleaner.
Proposal 2: Introduce a new
NumericChannel.get_last()
The only way I see of implementing this while avoiding sync RPCs would be to add a new
NumericChannel.get_last()
method.Comments:
It's a bit disappointing to need to add this on top of the existing sink interface, but arguably less so than having to go via the host to pass results between different fragments executing inside the same kernel!