Skip to content

Commit 99684b8

Browse files
authored
Merge pull request #124 from scipp/inelastic-sample
Add inelastic sample component that changes neutron wavelengths
2 parents f953256 + ce486d0 commit 99684b8

File tree

15 files changed

+1380
-576
lines changed

15 files changed

+1380
-576
lines changed

docs/api-reference/index.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212
1313
Chopper
1414
ChopperReading
15+
Component
1516
ComponentReading
1617
Dashboard
1718
Detector
1819
DetectorReading
20+
InelasticSample
1921
Model
2022
ReadingField
2123
Result
2224
Source
23-
SourceParameters
25+
SourceReading
2426
```
2527

2628
## Top-level functions
@@ -39,5 +41,6 @@
3941
:template: module-template.rst
4042
:recursive:
4143
44+
facilities
4245
utils
4346
```

docs/components.ipynb

Lines changed: 199 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
"metadata": {},
1919
"outputs": [],
2020
"source": [
21+
"import matplotlib.pyplot as plt\n",
22+
"import numpy as np\n",
2123
"import scipp as sc\n",
24+
"import plopp as pp\n",
2225
"import tof\n",
2326
"\n",
2427
"meter = sc.Unit('m')\n",
@@ -412,7 +415,191 @@
412415
"id": "33",
413416
"metadata": {},
414417
"source": [
415-
"## Loading from a JSON file\n",
418+
"## Inelastic sample\n",
419+
"\n",
420+
"Placing an `InelasticSample` in the instrument will change the energy of the incoming neutrons by a $\\Delta E$ defined in a function.\n",
421+
"That function takes in the incident energy and returns the final energy after the energy transfer took place.\n",
422+
"\n",
423+
"To give an equal chance for a random $\\Delta E$ between -0.2 and 0.2 meV, we can use Numpy's uniform random sampling:"
424+
]
425+
},
426+
{
427+
"cell_type": "code",
428+
"execution_count": null,
429+
"id": "34",
430+
"metadata": {},
431+
"outputs": [],
432+
"source": [
433+
"rng = np.random.default_rng(seed=83)\n",
434+
"\n",
435+
"\n",
436+
"def uniform_deltae(e_i):\n",
437+
" # Uniform sampling between -0.2 and 0.2 meV\n",
438+
" de = sc.array(\n",
439+
" dims=e_i.dims, values=rng.uniform(-0.2, 0.2, size=e_i.shape), unit='meV'\n",
440+
" )\n",
441+
" return e_i.to(unit='meV', copy=False) - de\n",
442+
"\n",
443+
"\n",
444+
"sample = tof.InelasticSample(distance=28.0 * meter, name=\"sample\", func=uniform_deltae)\n",
445+
"sample"
446+
]
447+
},
448+
{
449+
"cell_type": "markdown",
450+
"id": "35",
451+
"metadata": {},
452+
"source": [
453+
"We then make a single fast-rotating chopper with one small opening,\n",
454+
"to select a narrow wavelength range at every rotation.\n",
455+
"\n",
456+
"We also add a monitor before the sample, and a detector after the sample so we can follow the changes in energies/wavelengths."
457+
]
458+
},
459+
{
460+
"cell_type": "code",
461+
"execution_count": null,
462+
"id": "36",
463+
"metadata": {},
464+
"outputs": [],
465+
"source": [
466+
"choppers = [\n",
467+
" tof.Chopper(\n",
468+
" frequency=70.0 * Hz,\n",
469+
" open=sc.array(dims=['cutout'], values=[0.0], unit='deg'),\n",
470+
" close=sc.array(dims=['cutout'], values=[1.0], unit='deg'),\n",
471+
" phase=0.0 * deg,\n",
472+
" distance=20.0 * meter,\n",
473+
" name=\"fastchopper\",\n",
474+
" ),\n",
475+
"]\n",
476+
"\n",
477+
"detectors = [\n",
478+
" tof.Detector(distance=26.0 * meter, name='monitor'),\n",
479+
" tof.Detector(distance=32.0 * meter, name='detector'),\n",
480+
"]\n",
481+
"\n",
482+
"source = tof.Source(facility='ess', neutrons=5_000_000)\n",
483+
"\n",
484+
"model = tof.Model(source=source, components=choppers + detectors + [sample])\n",
485+
"res = model.run()\n",
486+
"\n",
487+
"\n",
488+
"fig, ax = plt.subplots(1, 2, figsize=(12, 4.5))\n",
489+
"\n",
490+
"dw = sc.scalar(0.1, unit='angstrom')\n",
491+
"pp.plot(\n",
492+
" {\n",
493+
" 'monitor': res['monitor'].data.hist(wavelength=dw),\n",
494+
" 'detector': res['detector'].data.hist(wavelength=dw),\n",
495+
" },\n",
496+
" title=\"With inelastic sample\",\n",
497+
" xmin=4,\n",
498+
" xmax=20,\n",
499+
" ymin=-20,\n",
500+
" ymax=400,\n",
501+
" ax=ax[1],\n",
502+
")\n",
503+
"\n",
504+
"res.plot(visible_rays=10000, ax=ax[0])"
505+
]
506+
},
507+
{
508+
"cell_type": "markdown",
509+
"id": "37",
510+
"metadata": {},
511+
"source": [
512+
"### Non-uniform energy-transfer distributions"
513+
]
514+
},
515+
{
516+
"cell_type": "code",
517+
"execution_count": null,
518+
"id": "38",
519+
"metadata": {},
520+
"outputs": [],
521+
"source": [
522+
"# Sample 1: double-peak at min and max\n",
523+
"def double_peak(e_i):\n",
524+
" # Either -0.2 or 0.2 meV\n",
525+
" de = sc.array(\n",
526+
" dims=e_i.dims, values=rng.choice([-0.2, 0.2], size=e_i.shape), unit='meV'\n",
527+
" )\n",
528+
" return e_i.to(unit='meV', copy=False) - de\n",
529+
"\n",
530+
"\n",
531+
"sample1 = tof.InelasticSample(\n",
532+
" distance=28.0 * meter,\n",
533+
" name=\"sample\",\n",
534+
" func=double_peak,\n",
535+
")\n",
536+
"\n",
537+
"\n",
538+
"# Sample 2: normal distribution\n",
539+
"def normal_deltae(e_i):\n",
540+
" de = sc.array(\n",
541+
" dims=e_i.dims, values=rng.normal(scale=0.05, size=e_i.shape), unit='meV'\n",
542+
" )\n",
543+
" return e_i.to(unit='meV', copy=False) - de\n",
544+
"\n",
545+
"\n",
546+
"sample2 = tof.InelasticSample(\n",
547+
" distance=28.0 * meter,\n",
548+
" name=\"sample\",\n",
549+
" func=normal_deltae,\n",
550+
")"
551+
]
552+
},
553+
{
554+
"cell_type": "code",
555+
"execution_count": null,
556+
"id": "39",
557+
"metadata": {},
558+
"outputs": [],
559+
"source": [
560+
"model1 = tof.Model(source=source, components=choppers + detectors + [sample1])\n",
561+
"model2 = tof.Model(source=source, components=choppers + detectors + [sample2])\n",
562+
"\n",
563+
"res1 = model1.run()\n",
564+
"res2 = model2.run()\n",
565+
"\n",
566+
"fig, ax = plt.subplots(2, 2, figsize=(12, 9))\n",
567+
"\n",
568+
"res1.plot(visible_rays=10000, ax=ax[0, 0], title=\"Sample 1\")\n",
569+
"pp.plot(\n",
570+
" {\n",
571+
" 'monitor': res1['monitor'].data.hist(wavelength=dw),\n",
572+
" 'detector': res1['detector'].data.hist(wavelength=dw),\n",
573+
" },\n",
574+
" title=\"Sample 1\",\n",
575+
" xmin=4,\n",
576+
" xmax=20,\n",
577+
" ymin=-20,\n",
578+
" ymax=400,\n",
579+
" ax=ax[0, 1],\n",
580+
")\n",
581+
"\n",
582+
"res2.plot(visible_rays=10000, ax=ax[1, 0], title=\"Sample 2\")\n",
583+
"_ = pp.plot(\n",
584+
" {\n",
585+
" 'monitor': res2['monitor'].data.hist(wavelength=dw),\n",
586+
" 'detector': res2['detector'].data.hist(wavelength=dw),\n",
587+
" },\n",
588+
" title=\"Sample 2\",\n",
589+
" xmin=4,\n",
590+
" xmax=20,\n",
591+
" ymin=-20,\n",
592+
" ymax=400,\n",
593+
" ax=ax[1, 1],\n",
594+
")"
595+
]
596+
},
597+
{
598+
"cell_type": "markdown",
599+
"id": "40",
600+
"metadata": {},
601+
"source": [
602+
"## Loading components from a JSON file\n",
416603
"\n",
417604
"It is also possible to load components from a JSON file,\n",
418605
"which can be very useful to quickly load a pre-configured instrument.\n",
@@ -423,19 +610,14 @@
423610
{
424611
"cell_type": "code",
425612
"execution_count": null,
426-
"id": "34",
613+
"id": "41",
427614
"metadata": {},
428615
"outputs": [],
429616
"source": [
430617
"import json\n",
431618
"\n",
432619
"params = {\n",
433-
" \"source\": {\n",
434-
" \"type\": \"source\",\n",
435-
" \"facility\": \"ess\",\n",
436-
" \"neutrons\": 1e6,\n",
437-
" \"pulses\": 1\n",
438-
" },\n",
620+
" \"source\": {\"type\": \"source\", \"facility\": \"ess\", \"neutrons\": 1e6, \"pulses\": 1},\n",
439621
" \"chopper1\": {\n",
440622
" \"type\": \"chopper\",\n",
441623
" \"frequency\": {\"value\": 56.0, \"unit\": \"Hz\"},\n",
@@ -470,7 +652,7 @@
470652
{
471653
"cell_type": "code",
472654
"execution_count": null,
473-
"id": "35",
655+
"id": "42",
474656
"metadata": {},
475657
"outputs": [],
476658
"source": [
@@ -479,7 +661,7 @@
479661
},
480662
{
481663
"cell_type": "markdown",
482-
"id": "36",
664+
"id": "43",
483665
"metadata": {},
484666
"source": [
485667
"We now use the `tof.Model.from_json()` method to load our instrument:"
@@ -488,7 +670,7 @@
488670
{
489671
"cell_type": "code",
490672
"execution_count": null,
491-
"id": "37",
673+
"id": "44",
492674
"metadata": {},
493675
"outputs": [],
494676
"source": [
@@ -498,7 +680,7 @@
498680
},
499681
{
500682
"cell_type": "markdown",
501-
"id": "38",
683+
"id": "45",
502684
"metadata": {},
503685
"source": [
504686
"We can see that all components have been read in correctly.\n",
@@ -508,7 +690,7 @@
508690
{
509691
"cell_type": "code",
510692
"execution_count": null,
511-
"id": "39",
693+
"id": "46",
512694
"metadata": {},
513695
"outputs": [],
514696
"source": [
@@ -517,7 +699,7 @@
517699
},
518700
{
519701
"cell_type": "markdown",
520-
"id": "40",
702+
"id": "47",
521703
"metadata": {},
522704
"source": [
523705
"### Modifying the source\n",
@@ -531,7 +713,7 @@
531713
{
532714
"cell_type": "code",
533715
"execution_count": null,
534-
"id": "41",
716+
"id": "48",
535717
"metadata": {},
536718
"outputs": [],
537719
"source": [
@@ -542,7 +724,7 @@
542724
},
543725
{
544726
"cell_type": "markdown",
545-
"id": "42",
727+
"id": "49",
546728
"metadata": {},
547729
"source": [
548730
"## Saving to JSON\n",
@@ -553,7 +735,7 @@
553735
{
554736
"cell_type": "code",
555737
"execution_count": null,
556-
"id": "43",
738+
"id": "50",
557739
"metadata": {},
558740
"outputs": [],
559741
"source": [

src/tof/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
submodules=['facilities'],
1818
submod_attrs={
1919
'chopper': ['AntiClockwise', 'Chopper', 'ChopperReading', 'Clockwise'],
20+
'component': ['Component', 'ComponentReading', 'ReadingField'],
2021
'dashboard': ['Dashboard'],
2122
'detector': ['Detector', 'DetectorReading'],
23+
'inelastic': ['InelasticSample', 'InelasticSampleReading'],
2224
'model': ['Model'],
23-
'reading': ['ComponentReading', 'ReadingField'],
2425
'result': ['Result'],
25-
'source': ['Source', 'SourceParameters'],
26+
'source': ['Source', 'SourceReading'],
2627
},
2728
)
2829

0 commit comments

Comments
 (0)