-
Notifications
You must be signed in to change notification settings - Fork 3
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
Frame unwrapping and WFM #472
Conversation
I had a short look but I think it would be much easier for me if we looked at the graphs together and you can explain the different steps, as well as the differences between the different graphs. |
src/scippneutron/tof/unwrap.py
Outdated
# Find last frame before ltotal | ||
frame_before_detector = None | ||
for frame in frames: | ||
if frame.distance > ltotal: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just checking: I guess we are guaranteed that the frames are ordered according to distance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, chopper_toolkit
sorts the choppers before applying them.
choppers: Choppers, source_chopper_name: SourceChopperName | ||
choppers: Choppers, | ||
source_time_range: SourceTimeRange, | ||
source_chopper_name: Optional[SourceChopperName], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should source_chopper_name
have a default None
value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sciline does not use default values.
are used to define a fake chopper. | ||
""" | ||
if source_chopper_name is not None: | ||
return choppers[source_chopper_name] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if the source_chopper_name
exists, then source_time_range
is unused. Should that also be an optional input?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, because then the pipeline would fail at runtime if neither is given?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Say you know the source chopper name, but not the source time range, you'd still have to supply a source time range as input.
If you don't know it, you could always make it to be nonsense because it won't be used, but it seems a little strange to require it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without a source time range the whole chopper-cascade math won't work. This is required anyway for other parts of the workflow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we then have a check to make sure that the source_time_range
is consistent with the source chopper if source_chopper_name
is given?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would that be possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we check that the chopper open and close times match the source_time_range
? Or maybe I misunderstood something and in some part of the pipeline you need the source chopper, and in others you need the source_time_range
and they can in principle be different? (e.g. if the source chopper is not at a distance of 0.0?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "source chopper" may define a different time range, i.e., it is a pulse-shaping chopper to increase the resolution. It will typically have a tighter time range.
src/scippneutron/tof/unwrap.py
Outdated
|
||
|
||
def offset_to_time_of_flight_wfm( | ||
time_offset: OffsetFromWrapped, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this may actually be wrong. I think we need the unwrapped time offset, not just the offset from wrapped to perform the WFM TOF offsetting.
Converting to draft until verified.
wavelength_max=source_wavelength_range[-1], | ||
) | ||
frames = frames.chop(choppers.values()) | ||
return FrameAtDetector(frames[ltotal]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this assume:
- that the neutrons move in straight lines to the detector elements?
- that there is no inelastic scattering?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. See my comment in the PR:
This clearly makes a bunch of assumptions which may turn out to be wrong in practice. At this point I have little confidence that this will be anything close to the final solution (or even that there is a solution at all — in the end we might still be forced to fall back to defining bounds more manually).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have anything enlightening to say. This just looks like what we talked about.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in l73: "For, e.g., detector 1 when then observe:\n",
should be "For, e.g., detector 1 we then observe:\n",
. And 'then' can be left out, too.
" $$\n", | ||
" \n", | ||
"Note that there are other valid definitions of $t_\\text{pivot}$, differing only in how neutrons in the intermediate \"background\" region (region between two pulses) are mapped to frames." | ||
"As illustrated in the figure, the pivot time $t_\\text{pivot}$ depends on the detector or rather the distance of the detector (or monitor) from the scattering position.\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the diagram, the pivot for detector 1 is at the black dashed line, i.e., the beginning of the orange frame. But in detector 2 it is in the middle between the frames.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"metadata": {}, | ||
"source": [ | ||
"At the sample or a detector we would only see this for a particular `Ltotal`:" | ||
"Choppers may be used to skip pulses, for the purpose of a simultaneous study of a wider wavelength range.\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by simultaneous
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use every pulse, measure 1-4 Angstrom, change the choppers, measure 4-8 Angstrom, and combine the results. That would be serial, as opposed to simultaneous (I think this is the term I have heard scientists use).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of questions.
Also, maybe I've asked this before, but why is this notebook in the API reference? It makes it pretty difficult to find in the docs I feel.
src/scippneutron/tof/unwrap.py
Outdated
@@ -378,7 +388,9 @@ def time_of_flight_origin_from_chopper( | |||
source_time_open = source_chopper.time_open[0] | |||
source_time_close = source_chopper.time_close[0] | |||
time_zero = 0.5 * (source_time_open + source_time_close) | |||
return TimeOfFlightOrigin(time_zero) | |||
return TimeOfFlightOrigin( | |||
time=time_zero, L1=sample_distance - source_chopper.distance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the TimeOfFlightOrigin
ill-defined if there is no sample in the run, or if you are trying to unwrap times for a monitor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not matter if there is no sample, this is about the definition of L1. This (or sample_position
) has to be defined even if there is no "sample", since all the coord transforms in ScippNeutron rely on it.
But you have actually stumbled upon a related problem: If we are unwrapping a monitor then the coord transforms defined in ScippNeutron do not use L1
but Ltotal
, so we should be correcting that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nvaytet Please have a look at the latest commit, this should fix the aforementioned issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Remember to add a test that checks for this when you get round to adding tests.
src/scippneutron/tof/unwrap.py
Outdated
return TimeOfFlightOrigin(sc.DataArray(neg_shift, coords={'subframe': times})) | ||
return TimeOfFlightOrigin( | ||
time=sc.DataArray(neg_shift, coords={'subframe': times}), | ||
L1=sample_distance - source_chopper.distance, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should they be distances or should they be defined as vector positions in space and take the norm of the difference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The chopper_cascade
using a simple distance, so this is what we have to use here. This will hopefully allow for hiding details such as curved guides (we do not want to chopper_cascade
toolkit to deal with such complications, if possible).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't really understand the WFM case and why it's necessary to treat it separately. My understanding was that given any chopper configuration we can propagate the source pulse through the cascade and compute the wavelength-time diagram at every detector(element), and from it + the event time data we can compute the time of flight. From that perspective the WFM case is just one particular chopper configuration and it's not clear to me why it needs a separate code path.
But this shouldn't be an obstacle to merge this, just a heads up that this is probably something we should talk about again.
TimeOffset = NewType('TimeOffset', sc.Variable) | ||
"""Unwrapped time offset relative to the pulse time.""" | ||
|
||
DeltaFromWrapped = NewType('DeltaFromWrapped', sc.Variable) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a docstring to describe what DeltaFromWrapped
represents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you review an outdated diff?
@@ -311,7 +314,7 @@ def offset_from_wrapped( | |||
+ sc.concat([frame_period, sc.zeros_like(frame_period)], dim), | |||
coords={dim: time.to(unit=elem_unit(wrapped_time_offset))}, | |||
) | |||
return OffsetFromWrapped(sc.lookup(offset, dim=dim)[wrapped_time_offset]) | |||
return DeltaFromWrapped(sc.lookup(offset, dim=dim)[wrapped_time_offset]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to use interpolate
here? If so, is there a reason why lookup
is advantageous
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scipp.lookup
can handle event data, interpolate
cannot, and may be slower.
src/scippneutron/tof/unwrap.py
Outdated
time_offset: OffsetFromWrapped, | ||
source_chopper: SourceChopper, | ||
) -> OffsetFromTimeOfFlight: | ||
def time_of_flight_origin(source_chopper: SourceChopper) -> TimeOfFlightOrigin: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do the user have to define the SourceChopper
? Can't that be automatically inferred from the description of the chopper setup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, not really, it is not necessarily the first chopper, and it might not even be an actual chopper, but a "virtual" one that one might define (e.g., to use the source pulse as "source chopper").
Co-authored-by: Jan-Lukas Wynen <[email protected]> Co-authored-by: Neil Vaytet <[email protected]>
Co-authored-by: Jan-Lukas Wynen <[email protected]>
It used to be more visible, but was moved there during the switch to the new theme. @jl-wynen can comment on why it is here or where we can move it. |
It does not a different code part, for the most part. But the time-of-flight origin needs to be defined differently, in particular different neutrons within a frame have a different origin. |
Setting L1 would have been broken for monitors. Furthermore, it might have been ignored if Ltotal was computed previously.
Green light from me for this round of review. Do I remember correctly that you said you were going to add tests after this round of review? |
@nvaytet Tests updated! |
@@ -505,14 +508,21 @@ def to_time_of_flight( | |||
if da.bins is not None: | |||
da = da.copy(deep=False) | |||
da.data = sc.bins(**da.bins.constituents) | |||
da.bins.coords['tof'] = time_offset + delta | |||
da.bins.coords['time_zero'] = da.bins.coords['pulse_time'] - delta.to( | |||
da.bins.coords['tof'] = time_offset - delta |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
This represents the latest approach to frame unwrapping and WFM (see old #415).
This clearly makes a bunch of assumptions which may turn out to be wrong in practice. At this point I have little confidence that this will be anything close to the final solution (or even that there is a solution at all — in the end we might still be forced to fall back to defining bounds more manually).
Testing is obviously challenging, and given that it is still not clear how all the choppers will be handled, I believe we must iterate on the whole solution (including what @jl-wynen has kicked of with #466) in the coming months.
Notes:
tof.frames
was removed.tof.fakes
, which I hope we can develop further for these and similar tests. This is currently my only idea on how to move forward: Build more realistic fakes, fix the chopper and unwrapping code, iterate.event_time_zero
. This will be essential for experiments with high time-resolution requirements.scippneutron.tof.chopper_cascade
modulescipp.lookup
overscipp.where
andscipp.bin
during unwrapping and WFM stitching (respectively) has equivalent or better performance. I think it is also clearer.Fixes scipp/ess#165.
Follow-up work:
tof.chopper_cascade
? #482tof.fakes
#483tof.chopper_cascade
#469