-
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
Update of WFM tof workflow to take into account DREAM choppers #560
Conversation
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.
Big congrats on getting this to work! 👍
I have only looked at the code changes, not the notebooks. Maybe a walkthrough would be best?
----- | ||
To find the time-of-flight origin, we ray-trace the fastest and slowest neutrons of | ||
the subframes back to the first chopper to determine the time-of-flight origin. | ||
The assumption here is that the first chopper is one of the two wfm choppers. |
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 this the case for all ESS instruments with WFM?
src/scippneutron/tof/unwrap.py
Outdated
# Ray-trace back to the position of the first chopper (note that frame 0 is the | ||
# pulse itself) | ||
distance = frames[1].distance | ||
dist = ltotal - distance | ||
t1 = tmin - dist * chopper_cascade.wavelength_to_inverse_velocity(wmin) | ||
t2 = tmax - dist * chopper_cascade.wavelength_to_inverse_velocity(wmax) |
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.
chopper_cascade
can propagate forwards, e.g., to the detector position. Can't the same code be used to go backwards?
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.
That would be great, but I wasn't immediately able to figure that out. Would be super if you can help with 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.
I think you could just use
def propagate_times( |
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.
Or use
def propagate_by(self, distance: sc.Variable) -> Subframe: |
if we can have a proper frame setup?
# We currently have no way of detecting which cutouts of the "source" chopper | ||
# are used (not blocked by other choppers), for this test we remove those we | ||
# know are not used. |
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 replace by comment that explains (and assertion) that there are extra openings but the code knows how to deal with them automatically?
"source": [ | ||
"# DREAM in WFM mode\n", | ||
"\n", | ||
"In this notebook, we show how to use Scippneutron's `tof` module to find the boundaries of the WFM frames for the DREAM instrument.\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 this notebook, we show how to use Scippneutron's `tof` module to find the boundaries of the WFM frames for the DREAM instrument.\n", | |
"In this notebook, we show how to use ScippNeutron's `tof` module to find the boundaries of the WFM frames for the DREAM instrument.\n", |
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"psc1 = DiskChopper(\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.
Is this setup for the case of overlapping subframes, as in the McStas data? I have not seen how that is being handled?
start_rotation: | ||
This can be used to include rotations before the first pulse by giving a | ||
negative integer. This is useful for choppers that are not in phase with | ||
the source. The default is 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.
Is there ever a case where we don't want to include such rotations? That is, is this argument even needed?
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.
Changed this to now always have the -1 rotation.
n_repetitions: | ||
Return this many times for each angle corresponding to multiple rotations | ||
of the chopper. | ||
start_rotation: | ||
This can be used to include rotations before the first pulse by giving a | ||
negative integer. This is useful for choppers that are not in phase with | ||
the source. The default is 0. | ||
end_rotation: | ||
How many chopper rotations to perform. |
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.
Same question as above: Maybe the existing argument was fine and we should just ensure that we always include the start?
# If end is after current end, there is only partial overlap, so we clip | ||
if bound.end > current.end: | ||
old_end = current.end | ||
old_wav_end = current.wav_end | ||
current = Bound( | ||
current.start, | ||
bound.start, | ||
current.wav_start, | ||
bound.wav_start, | ||
) | ||
merged_bounds.append(current) | ||
current = Bound( | ||
old_end, | ||
bound.end, | ||
old_wav_end, | ||
bound.wav_end, | ||
) | ||
# If end is before current end, overlap is total and we merge |
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 get this. What is being clipped? What is meant be "overlap is partial" and why don't we just want to merge?
Note that CI will fail until scipp/scipp#3582 is merged and released. |
@SimonHeybrock I believe this is ready for another look. |
# Start at -1 to ensure a rotation that is finishing when the pulse begins is | ||
# also included. |
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.
Not sure what this comment relates to?
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.
Removed. Was left over from previous changes.
src/scippneutron/tof/unwrap.py
Outdated
sample. The frame bounds are then computed from this. | ||
""" | ||
return FrameAtDetector(frames[-1].propagate_to(ltotal)) | ||
# 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.
Commented code?
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, I actually left it in there intentionally because I wasn't sure if I should use the getitem
syntax.
I believe I needed to make the change because inside the getitem
it assumes that the distance passed is a scalar.
Should I fix in the getitem
or should I keep the code I have now?
To fix in the getitem
, we would have to handle cases where you propagate to a range of distances, some might be before the last frame, some might be after... It gets messy.
I could also just raise a helpful error message if the distance passed in the getitem
is not a scalar?
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 a strong opinion, do what you think works best.
In the case of multiple detector pixels, we find the pixel closest to the source | ||
and use that as the reference for the clipping. |
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.
Hmm, shouldn't you get more overlap for pixels with the greatest 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.
I think it doesn't matter because it will just propagate to the other distances and there are no more choppers in the way.
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.
My point was that if there are two subframes that to not overlap at L2=5 meter, they may overload at any greater L2. Or do I misunderstand the comment?
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, you're right, the frames may not overlap on the closest pixel.
However, if we take the overlapping times at the farthest pixel, and use those to make a fake chopper, those times would not match the overlap seen at the closest pixel, they would be shifted to the right.
So the closest frame would have a small region where there is still overlap.
Basically, I think I need to find the overlap at the farthest pixel, and propagate those backwards to the closest pixel distance, and then make my fake chopper...
As discussed, this fake chopper implementation is not great.
@jokasimr and I do have a prototype of an alternative method which is working.
Question is: should we spend time on fixing this, or should we just try to get that into a production shape instead of trying to fix this implementation?
We could leave this one as is and assume this edge case won't be an issue in the next couple of weeks.
We then can roll out the alternative version a short time after?
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 is it an edge case? Doesn't it affect all (WFM) beamlines that do not have pixels on a sphere? Or do you mean the need for clipping is an edge case (unusual DREAM settings?).
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 mean that both the DREAM choppers are a little unusual, and that the case where some pixels have overlap and others don't is hopefully an edge case as the range of pixel distances is not too large for a given instrument?
(I guess Loki is the odd one out, but they don't have WFM choppers)
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.
Then we should just use the furthest right (and live with throwing away a tiny bit too many events for the closer pixels)? Otherwise you always get overlap for all but the closest pixel.
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.
Let's find a time to chat in a call.
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.
Conclusion from the call: because we are making a gap when adding the fake chopper rather than clipping with an infinitely thin vertical line, the distance it would take for the gap to completely fill in again with overlap is hopefully larger than the range of pixel distances.
So the overlap should be removed for all pixels (in most cases).
of the WFM choppers. | ||
Check for time overlap between subframes. | ||
If overlap is found, we clip away the regions where there is overlap. | ||
This is done by adding a fake chopper which is closed during the overlapping times. |
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.
Not clear to me why we still need choppers at this point. Instead of adding a fake chopper, can't we just clip the (sub)frame bounds?
# We cannot chop frames at multiple distances at once, so instead of chopping the | ||
# frame at the detector, we chop the last frame in the chopper cascade before the | ||
# detector. | ||
last_frame = chopper_cascade_frames[-1] | ||
|
||
# Chop the subframes one by one | ||
# TODO: We currently need to chop them one by one as the `chop` method seems | ||
# to not work correctly with overlapping subframes. | ||
subframes = [] | ||
for subframe in last_frame.subframes: | ||
f = chopper_cascade.Frame(distance=last_frame.distance, subframes=[subframe]) | ||
chopped = f.chop(fake_chopper) | ||
subframes.extend( | ||
[ | ||
sf | ||
for sf in chopped.subframes | ||
if not sc.allclose(sf.start_time, sf.end_time) | ||
] | ||
) |
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.
All this (and above) looks really complicated (read: very difficult to verify). I wonder if it is due to the fake-chopper. Why can't we just compute bounds, clip, and continue?
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 am using chop
because I need to have a well-constructed frame, that I can then propagate backwards to find the time origin.
I could just clip the time range, but I also want to clip the wavelengths in the correct way, so the backward propagation is correct. I thought the best way was to use chop
.
Do you have a another suggestion?
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 frame-propagation is already a free function. Wouldn't it still be easier to compute time and wavelength bounds directly?
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.
Finding the time bounds is easy enough (it's just about sorting the time bounds along the subframe dimension).
Do you have a vectorized way of clipping the subframe to find the wavelengths?
The code in the _chop()
function does not handle this, and I can't see an easy way to adapt 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.
I honestly do not remember, it has been too long since I wrote this. We can have a look together one day if you like?
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.
Hmm I think I may have a way to do this, by taking the mean of the intersection from all vertices of the subframe.
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've looked into simplifying this without using the fake chopper, for many pixels in a vectorized way. It's proving difficult.
My suggestion would be to merge this implementation so that it doesn't block all the other workflows.
I will revisit right after with a simpler approach but it should only be internal refactoring and shouldn't change how it is used by workflows.
src/scippneutron/tof/unwrap.py
Outdated
# TODO: what dimension order is the best here? Where should `detector_number` be? | ||
origin_time = sc.DataArray(shift.transpose(times.dims), coords={'subframe': times}) |
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.
If this is meant for processing the raw data, detector_number
should be first (slowest dim), since that is how data is sorted.
src/scippneutron/tof/unwrap.py
Outdated
# TODO: Can we do without making a copy of delta? | ||
delta = sc.lookup(delta.copy(deep=True), dim='subframe')[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.
Shouldn't/isn't sc.lookup
doing this?
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, I think you're right.
Co-authored-by: Simon Heybrock <[email protected]>
@SimonHeybrock any final comments? |
The DREAM pulse-shaping choppers (PSC) are different from the V20 or ODIN WFM choppers in the way that they have 8 cutouts that can shape the pulse in different ways.
In the example McStas simulation that we have, the detector sees 2 frames made from 8 cutouts.
The workflow that computed time-of-flight from event-time-offset that we had assumed that the number of frames was equal to number of cutouts.
This PR updates the workflow to handle both cases.
Note that we re-wrote the workflow graph to simplify it, from
to