-
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
Alternative to time-origin tof computation #577
Conversation
…providers and types to unwrap
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.
Overall this looks more or less complete to me. Great job. (-300 SLOC is nice)
Left a couple of comments of things I think we should change before merging.
src/scippneutron/tof/unwrap.py
Outdated
a /= weight.sum(dims) | ||
# Compute the intercept as the mean of the intercepts of the curves that pass | ||
# through the vertices | ||
b = (y - a * x).mean('vertex') |
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 we should use another method to determine the line that best approximates the polygon.
While this method works well in the cases we've seen so far it is not obvious that it will always produce something reasonable.
For example, if two vertices have almost the same x-coord the slope will grow a lot and the average slope might be larger than expected.
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 forgot to look at the notebook you sent.
Thanks for reminding me.
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.
src/scippneutron/tof/unwrap.py
Outdated
# begin of the first bin, there may be a significant gap (which we could fill | ||
# with NaNs or zeros), but there could also be a small overlap. | ||
return UnwrappedData(da) | ||
return TofCoord(toa) |
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 not assume the neutron left in the middle of the pulse? (Just because that makes a slightly better estimate.)
That is, shouldn't this be something like toa - sc.scalar(3, unit='ms')/2
?
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.
Yeah, good point, we could do that 👍
src/scippneutron/tof/unwrap.py
Outdated
unsorted bin edges (due to either wrapping of `time_of_flight` or wavelength | ||
overlap between subframes). | ||
This function re-histograms the data to ensure that the bin edges are sorted. | ||
It generates a number of events in each bin with a normal distribution around |
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 seems to me this function is essentially making a change of variables in a distribution function (or histogram).
That is, starting with a distribution over toa
and a function tof(toa)
, we want to compute the distribution over tof
.
To do this we should use the formula here: https://en.wikipedia.org/wiki/Probability_density_function#Function_of_random_variables_and_change_of_variables_in_the_probability_density_function (the not monotonic case)
If we used that I'm sure we'd end up with a simpler solution 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.
If you could give me some pointers on how you would implement this, I'd be happy to try. I don't think I fully understood what's in the link.
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.
Note also that this function is just to return something which is like the original data, but can be used further for normalization etc.
The real data is actually the TofData
, but that has unsorted bin edges.
So maybe the details of this function are not super important because we can swap it out for a different/better provider later?
It was mostly designed so I could write some tests where I compare the wavelengths...
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 was looking at this now and it became more complicated than I thought it would be 🤔
What you've already written might be the better solution.
Like you said, if we come up with something later it can be revisited.
I'll take another look tomorrow! |
I'm still working on the review, it'll be done later today |
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 there might be a wrong assumption here that the minimum event_time_zero
in the file is also the start of a stride in pulse skipping mode.
But what might happen is that the user starts writing the file in the middle of the stride when pulse skipping is used.
Is that taken into consideration here?
Note that this is probably not an issue for simulated data.
src/scippneutron/tof/fakes.py
Outdated
# the boundaries have crossed, which means that the chopper is rotating | ||
# must rotate at a lower frequency. |
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 boundaries have crossed, which means that the chopper is rotating | |
# must rotate at a lower frequency. | |
# the boundaries have crossed, which means that the chopper | |
# is rotating at a lower frequency. |
""" | ||
if da.bins is None: | ||
# Canonical name in NXmonitor | ||
return PulseWrappedTimeOffset(da.coords['time_of_flight']) | ||
return PulseWrappedTimeOffset(da.bins.coords['event_time_offset']) | ||
toa = da.coords['time_of_flight'] |
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 time_of_flight
field already unwrapped? That is, does it denote true time of flight, or does it just denote time of arrival but has a misleading 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.
The latter. It's just the name that is given in NeXus files.
src/scippneutron/tof/unwrap.py
Outdated
# Hence we use the smallest event_time_zero as the time origin. | ||
time_zero = da.coords['event_time_zero'] - da.coords['event_time_zero'].min() | ||
coord = da.bins.coords['event_time_offset'] | ||
toa = coord + time_zero.to(dtype=float, unit=elem_unit(coord), copy=False) |
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 there might be something wrong here in the case when pulse skipping is used.
We know that
toa = event_time_offset + event_time_zero
= event_time_offset + frame_time_zero + frame_time_offset
where frame_time_zero
is the time the start of the latest "logical pulse" (the start of the most recent pulse that wasn't blocked by the pulse-skip chopper) and frame_time_offset = event_time_zero - frame_time_zero
.
From the chopper cascade we know that the frame takes frame_travel_time
to reach the detector.
Therefore the frame starts at start_time = frame_travel_time + frame_time_zero
.
Then
toa_minus_start_time_modulo_frame_period
= (toa - start_time) % frame_period
= (event_time_offset + event_time_zero - start_time) % frame_period
= (event_time_offset + frame_time_offset - frame_travel_time) % frame_period
so I'd suggest that we
- Remove
UnwrappedTimeOfArrival
since (it seems) it's only used to computeUnwrappedTimeOfArrivalMinusStartTime
. - Rename
FrameAtDetectorStartTime
toFrameTravelTime
. - Add a provider that computes
FrameTimeOffset
- this might be complicated. Typically I guess we have to get this from the chopper configuration somehow. For simulated data this is easy because thenframe_time_offset = (event_time_zero - minimum_event_time_zero) % frame_period
just like in the code 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.
I fixed the case where a pulse skipping chopper would have a phase offset of 180 deg by propagating multiple pulses through the chopper cascade to predict frame at detector location.
I fixed the potential issue of starting to record in the middle of a pulse-skipping frame by adding a PulseStrideOffset
integer parameter, which can be set by the user if the data doesn't look right, and which is set to 0 by default.
src/scippneutron/tof/unwrap.py
Outdated
time_zero = 0.5 * (source_time_open + source_time_close) | ||
return TimeOfFlightOrigin(time=time_zero, distance=source_chopper.distance) | ||
return TimeOfArrivalModuloPeriod( | ||
toa % frame_period.to(unit=elem_unit(toa), copy=False) |
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.
Related to the above comment. What we want here is the time since the most recent frame_time_zero
.
That is event_time_offset + frame_time_offset
.
In a simulation we know that the first event_time_zero
coincides with the first frame_time_zero
and then what is implemented here is correct.
…e stride we should start with when pulse skipping
… frame is missing from raw data
Looks good to me! |
This is an alternative implementation of the computation of the
tof
coordinate in theunwrap
submodule.The motivations are the following:
Simplify the code in
tof/unwrap.py
(finding the tof origin with WFM was complex with a fake chopper and propagating frames backwards along the beamline)Generalize the code so that we only have to use a single set of providers instead of having providers for standard unwrap, pulse-skipping and WFM (only the uncommon case where there are no choppers requires different providers).
Implementation
The implementation is basically described in https://tof.readthedocs.io/en/stable/wfm.html.
chopper_cascade
module to find the ranges of time and wavelength (polygons) that are reachable at the detector pixels.