Skip to content

Commit 79e3c11

Browse files
authored
Merge pull request #136 from scipp/source-optimize
Optimize source using chopper cascade acceptance diagram
2 parents 44b6858 + c9de354 commit 79e3c11

7 files changed

Lines changed: 1148 additions & 159 deletions

File tree

docs/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ sources
6363
components
6464
multiple-pulses
6565
wfm
66-
dashboard
66+
optimizations
6767
ess-instruments
68+
dashboard
6869
api-reference/index
6970
developer/index
7071
about/index

docs/optimizations.ipynb

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "0",
6+
"metadata": {},
7+
"source": [
8+
"# Optimizations\n",
9+
"\n",
10+
"## Optimize for a given chopper cascade\n",
11+
"\n",
12+
"Most times when we run a `tof` model,\n",
13+
"the vast majority of neutrons in a pulse get blocked by the first few choppers in the beam path.\n",
14+
"\n",
15+
"For example, using the chopper settings for the Odin (ESS) instrument:"
16+
]
17+
},
18+
{
19+
"cell_type": "code",
20+
"execution_count": null,
21+
"id": "1",
22+
"metadata": {},
23+
"outputs": [],
24+
"source": [
25+
"import scipp as sc\n",
26+
"import matplotlib.pyplot as plt\n",
27+
"import tof"
28+
]
29+
},
30+
{
31+
"cell_type": "code",
32+
"execution_count": null,
33+
"id": "2",
34+
"metadata": {},
35+
"outputs": [],
36+
"source": [
37+
"s1 = tof.Source(facility=\"ess\", neutrons=1_000_000)\n",
38+
"beamline = tof.facilities.ess.odin(pulse_skipping=True)\n",
39+
"m1 = tof.Model(source=s1, **beamline)\n",
40+
"r1 = m1.run()\n",
41+
"r1"
42+
]
43+
},
44+
{
45+
"cell_type": "markdown",
46+
"id": "3",
47+
"metadata": {},
48+
"source": [
49+
"We can see that out of 1M neutrons that left the source, over 900K were blocked by the first chopper.\n",
50+
"In the end, only ~57K make it to the detector.\n",
51+
"\n",
52+
"This is incredibly wasteful in both memory and compute.\n",
53+
"\n",
54+
"We can however use the information of the opening and closing times of the choppers to predict which parts of the pulse (in the `birth_time`/`wavelength` space) will make it through and which regions will be blocked.\n",
55+
"This is otherwise known as a 'chopper acceptance diagram'.\n",
56+
"\n",
57+
"This can be visualized by looking at the birth times and wavelengths of the neutrons that made it to the detector,\n",
58+
"and compare that to the original distribution of neutrons in the source."
59+
]
60+
},
61+
{
62+
"cell_type": "code",
63+
"execution_count": null,
64+
"id": "4",
65+
"metadata": {},
66+
"outputs": [],
67+
"source": [
68+
"fig1 = s1.data.hist(wavelength=300, birth_time=300).plot(norm='log', title=\"Sampled from source\")\n",
69+
"fig2 = r1['detector'].data.hist(wavelength=300, birth_time=300).plot(norm='log', title=\"Neutrons that make it to the detector\")\n",
70+
"\n",
71+
"fig1 + fig2"
72+
]
73+
},
74+
{
75+
"cell_type": "markdown",
76+
"id": "5",
77+
"metadata": {},
78+
"source": [
79+
"The source has an in-built method to apply the chopper acceptance criteria during the sampling of neutrons;\n",
80+
"it is activated via the `optimize_for` argument.\n",
81+
"It expects a list of choppers, and only neutrons that would make it through all choppers end up in the source."
82+
]
83+
},
84+
{
85+
"cell_type": "code",
86+
"execution_count": null,
87+
"id": "6",
88+
"metadata": {},
89+
"outputs": [],
90+
"source": [
91+
"# Filter out choppers from list of Odin components\n",
92+
"choppers = {comp.name: comp for comp in beamline['components'] if isinstance(comp, tof.Chopper)}\n",
93+
"\n",
94+
"# Create optimized source\n",
95+
"s2 = tof.Source(facility=\"ess\", neutrons=1_000_000, optimize_for=choppers)\n",
96+
"\n",
97+
"m2 = tof.Model(source=s2, **beamline)\n",
98+
"r2 = m2.run()\n",
99+
"r2"
100+
]
101+
},
102+
{
103+
"cell_type": "markdown",
104+
"id": "7",
105+
"metadata": {},
106+
"source": [
107+
"We can now see that **all 1M neutrons make to the detector**,\n",
108+
"and plotting the birth time/wavelength distribution illustrates the optimization:"
109+
]
110+
},
111+
{
112+
"cell_type": "code",
113+
"execution_count": null,
114+
"id": "8",
115+
"metadata": {},
116+
"outputs": [],
117+
"source": [
118+
"fig1 = s2.data.hist(wavelength=300, birth_time=300).plot(norm='log', title=\"Sampled from source\")\n",
119+
"fig2 = r2['detector'].data.hist(wavelength=300, birth_time=300).plot(norm='log', title=\"Neutrons that make it to the detector\")\n",
120+
"\n",
121+
"fig1 + fig2"
122+
]
123+
},
124+
{
125+
"cell_type": "markdown",
126+
"id": "9",
127+
"metadata": {},
128+
"source": [
129+
"It is also clear that the signal recorded at detector is much less noisy due to the improved statistics.\n",
130+
"It is also important to note that the overall shape of the data (relative intensities) was not changed by the optimization."
131+
]
132+
},
133+
{
134+
"cell_type": "code",
135+
"execution_count": null,
136+
"id": "10",
137+
"metadata": {},
138+
"outputs": [],
139+
"source": [
140+
"fig, ax = plt.subplots(1, 2, figsize=(12, 4))\n",
141+
"\n",
142+
"r1['detector'].toa.plot(ax=ax[0])\n",
143+
"r2['detector'].toa.plot(color='C1', ax=ax[0].twinx())\n",
144+
"\n",
145+
"r1['detector'].wavelength.plot(ax=ax[1])\n",
146+
"r2['detector'].wavelength.plot(color='C1', ax=ax[1].twinx())\n",
147+
"\n",
148+
"fig.tight_layout()"
149+
]
150+
},
151+
{
152+
"cell_type": "markdown",
153+
"id": "11",
154+
"metadata": {},
155+
"source": [
156+
"## Pulse skipping\n",
157+
"\n",
158+
"Some instruments (such as Odin) use a pulse skipping chopper to 'skip' every other pulse, thus allowing to record a wider wavelength range at the detector without having issues where neutrons from successive pulses mix (also known as pulse-overlap).\n",
159+
"\n",
160+
"In such a setup, when running multiple pulses, all neutrons from every other pulse are rendered useless.\n",
161+
"Yet, `tof` naively treats them as normal neutrons and tries to follow them all the way to the detector."
162+
]
163+
},
164+
{
165+
"cell_type": "code",
166+
"execution_count": null,
167+
"id": "12",
168+
"metadata": {},
169+
"outputs": [],
170+
"source": [
171+
"s1 = tof.Source(facility=\"ess\", neutrons=1_000_000, pulses=4)\n",
172+
"m1 = tof.Model(source=s1, **beamline)\n",
173+
"r1 = m1.run()\n",
174+
"r1.plot(blocked_rays=5000)"
175+
]
176+
},
177+
{
178+
"cell_type": "markdown",
179+
"id": "13",
180+
"metadata": {},
181+
"source": [
182+
"To avoid wasting all the neutrons in the second pulse, a simple trick is to override the frequency of the source.\n",
183+
"Here we set it to 7 Hz (half of the original 14 Hz), meaning that the second pulse above will not exist at all."
184+
]
185+
},
186+
{
187+
"cell_type": "code",
188+
"execution_count": null,
189+
"id": "14",
190+
"metadata": {},
191+
"outputs": [],
192+
"source": [
193+
"s2 = tof.Source(facility=\"ess\", neutrons=1_000_000, pulses=2, frequency=sc.scalar(7, unit=\"Hz\"))\n",
194+
"m2 = tof.Model(source=s2, **beamline)\n",
195+
"r2 = m2.run()\n",
196+
"r2.plot(blocked_rays=5000)"
197+
]
198+
},
199+
{
200+
"cell_type": "markdown",
201+
"id": "15",
202+
"metadata": {},
203+
"source": [
204+
"We have now used half as many neutrons to achieve the same results.\n",
205+
"In combination with the `optimize_for` option introduced above, these optimizations can lead to significant speedups."
206+
]
207+
}
208+
],
209+
"metadata": {
210+
"kernelspec": {
211+
"display_name": "Python 3 (ipykernel)",
212+
"language": "python",
213+
"name": "python3"
214+
},
215+
"language_info": {
216+
"codemirror_mode": {
217+
"name": "ipython",
218+
"version": 3
219+
},
220+
"file_extension": ".py",
221+
"mimetype": "text/x-python",
222+
"name": "python",
223+
"nbconvert_exporter": "python",
224+
"pygments_lexer": "ipython3",
225+
"version": "3.12.7"
226+
}
227+
},
228+
"nbformat": 4,
229+
"nbformat_minor": 5
230+
}

0 commit comments

Comments
 (0)