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

Add notebook for computing WFM time-of-flight #529

Merged
merged 31 commits into from
Sep 30, 2024
Merged

Add notebook for computing WFM time-of-flight #529

merged 31 commits into from
Sep 30, 2024

Conversation

nvaytet
Copy link
Member

@nvaytet nvaytet commented Jul 4, 2024

  • Add notebook that computes tof for neutron events from WFM beamline
  • Add a new provider to specify TimeOfFlightOrigin parameters manually.

Draft because no unit test yet, but still worth reviewing the notebook and changes in providers.

Copy link
Member

@SimonHeybrock SimonHeybrock left a comment

Choose a reason for hiding this comment

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

Small comments, looks good otherwise:

cls,
disk_chopper: DiskChopper,
pulse_frequency: sc.Variable,
npulses: int = 1,
Copy link
Member

@SimonHeybrock SimonHeybrock Jul 5, 2024

Choose a reason for hiding this comment

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

Maybe use a different (larger) default. I think there is probably no harm in making this larger than necessary. Alternatively, remove the default, since 1 is probably harmful, at least for ESS instrument.

@@ -404,6 +413,38 @@ def time_offset(unwrapped: UnwrappedData) -> TimeOffset:
return TimeOffset(unwrapped.coords['time_offset'])


def time_of_flight_origin_wfm(
distance: TimeOfFlightOriginDistance,
time_open: TimeOfFlightOriginTime,
Copy link
Member

Choose a reason for hiding this comment

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

Bad name, since it is not the time when the slits open? How about subframe_origin_time?

Comment on lines 444 to 445
# TODO: add a helper provider that can make time of flight origin from a pair of WFM
# choppers
Copy link
Member

Choose a reason for hiding this comment

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

I suppose then either the above or below functions goes way, since it is split into two as per our discussion?

"id": "360be644-4c0d-4a0c-9497-af5a603cbfe1",
"metadata": {},
"source": [
"## Creating some neutron events\n",
Copy link
Member

Choose a reason for hiding this comment

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

Note a lot of overlap above and below with the fakes that were being introduced in https://github.com/scipp/scippneutron/blob/main/src/scippneutron/tof/fakes.py. Can we extend and use that?

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to use FakeSource and FakeBeamline to create multiple pulses of neutrons that span different wavelength ranges, and then just merge all the events in a single data array.

Something like:

# Make the ESS source with tof
N = 500_000  # Number of neutrons in a pulse
source = tof.Source(facility='ess', neutrons=N)

# Histogram the wavelengths in coarse bins, to make 20 pulses below
neutrons_per_wavelength = source.data.squeeze().hist(wavelength=sc.linspace('wavelength', 0.05, 11.0, 21, unit='angstrom'))

choppers = {
    key: cc.Chopper.from_disk_chopper(
             chop,
             pulse_frequency=source.frequency,
             npulses=1,
         )
    for key, chop in disk_choppers.items()
}

events = None

for n in neutrons_per_wavelength:
    s = fakes.FakeSource(
        frequency=sc.scalar(14.0, unit='Hz'), run_length=sc.scalar(10.0, unit='s'),
        events_per_pulse=int(n.value)
    )

    p = fakes.FakePulse(
        time_min=sc.scalar(0.0, unit='ms'),
        time_max=sc.scalar(3.0, unit='ms'),
        wavelength_min=n.coords['wavelength'].min(),
        wavelength_max=n.coords['wavelength'].max(),
    )
    
    beamline = fakes.FakeBeamline(
        source=s,
        pulse=p,
        choppers=choppers,
        monitors={'monitor': Ltotal},
        detectors={},
    )
    # Need try/except as some wavelength ranges have 0 neutrons that make it through
    try:
        mon, ref = beamline.get_monitor('monitor')
        if events is None:
            events = mon
        else:
            events = events.bins.concatenate(mon)
    except (ValueError, NotImplementedError):
        pass

but the final events don't look great. The different pulses have sharp edges, and some overlap where we see spikes in the histogrammed plot:
Screenshot at 2024-07-05 14-03-58

Compared to when I generated the events with tof:
Screenshot at 2024-07-05 14-04-13

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm if I use more bins, I get something that's better. Maybe I can tweak this further...
Screenshot at 2024-07-05 14-24-12

"source": [
"## Using the chopper cascade to chop the pulse\n",
"\n",
"The `chopper_cascade` module (abbreviated here as `cc`) can now be used to chop a pulse of neutrons using the choppers created above.\n",
Copy link
Member

Choose a reason for hiding this comment

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

I'd avoid importing under an alias unless a module is used many times, since it just adds to the cognitive overhead. Spell it out!

Comment on lines 434 to 435
"# Use provider to set time of flight origin manually\n",
"pl.insert(uw.time_of_flight_origin_wfm)\n",
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to replace the old provider with one that uses a pair of WFM choppers.
I've also left the possibility of setting the time of flight origin and distance manually (I hope!).
See update.

Comment on lines 478 to 482
times = []
for ch in wfm_choppers:
times.append(ch.time_open)
times.append(ch.time_close)
return TimeOfFlightOriginTime(sc.concat(times, dim='dummy').mean('dummy'))
Copy link
Member

Choose a reason for hiding this comment

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

Comment on lines 410 to 432
"""
Compute the time-of-flight origin from a source chopper in the WFM case.

For WFM there is not a single time-of-flight "origin", but one for each subframe.
For each subframe, the time-of-flight origin may be defined as the center of the
respective chopper slit opening of the WFM chopper. In some cases there is a pair
WFM choppers, in which case the time-of-flight origin may be defined using some
combination of the two choppers. This is not supported yet, as we only support
a single source chopper input.
"""
if len(source_chopper.time_open) != subframe_bounds['time'].sizes['subframe']:
raise NotImplementedError(
"Source chopper openings do not match the subframe count, this is ."
"not supported yet."
)
""" """
Copy link
Member

Choose a reason for hiding this comment

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

Docstring removed by accident?

Comment on lines 442 to 443
# Select the first n time origins to match subframe bounds
subframe_origin_time = subframe_origin_time[: subframe_bounds.sizes['subframe']]
Copy link
Member

Choose a reason for hiding this comment

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

Um, why?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure what else to do here? The subframe_origin_time has maybe a length of e.g. 30, but we know the choppers only have 6 openings. The dimensions wouldn't agree further down without the slicing, I think?

Copy link
Member

Choose a reason for hiding this comment

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

Sure, but why the first 6? It could just as well be the last 6, or some in the middle?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is the last thing I have not yet addressed. But I am unsure how to proceed. Do you have a suggestion?

Copy link
Member

Choose a reason for hiding this comment

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

Not really, unfortunately. Require that only the relevant times and/or chopper openings are passed in?

Copy link
Member Author

Choose a reason for hiding this comment

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

See if the update is reasonable.

Comment on lines 448 to 453
shift = (
sc.concat([nans, subframe_origin_time], dim=uuid4().hex)
.transpose()
.flatten(to='subframe')
)
shift = sc.concat([shift, nans[0]], 'subframe')
Copy link
Member

Choose a reason for hiding this comment

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

Unused shift from above? Also, why not use full_liketo makeshift, then use setitem with [1::2]` to set the times?

@nvaytet nvaytet marked this pull request as ready for review September 18, 2024 11:12
Copy link
Member

@SimonHeybrock SimonHeybrock left a comment

Choose a reason for hiding this comment

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

👍 Fingers crossed that this is one of the last few iterations on the WFM work.

@nvaytet nvaytet merged commit 8a33f36 into main Sep 30, 2024
5 checks passed
@nvaytet nvaytet deleted the wfm-data branch September 30, 2024 08:04
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.

2 participants