Build the transmitted region¶
Region is the region a chopper train transmits, exactly — a union of convex polygons in
(inverse velocity, emission time).
Build one¶
Start from the rectangle your source draws from, and transmit it through the train:
from chopcal.lib import Region
# the rectangle the source draws from: wavelengths in AA, then emission time in s
source = Region.from_wavelengths(0.4, 45.0, 0.0, 3e-3)
region = source.transmit(train)
print(len(region), 'polygon(s)')
print(region.wavelength_ranges())
print(f'acceptance {region.area / source.area:.6e}')
Region.from_wavelengths(low, high, time_minimum, time_range) takes angstrom and seconds;
Region.rectangle takes inverse velocity directly if you have it.
For the six BIFROST discs the answer is one polygon of five vertices. That is the whole transmitted phase space of the instrument.
Why it is exact¶
A neutron emitted at inverse velocity a and time t reaches path L at t + L*a, so a
disc open on [lower, upper] accepts
a slab between two parallel lines. A train's acceptance is an intersection of unions of such slabs, and intersection distributes over union — so the exact region is a union of convex pieces, one per choice of which opening and which turn of each disc a neutron goes through. Nothing is approximated and nothing is quantised.
Read the pieces¶
for polygon in region.polygons:
print(len(polygon), 'vertices, area', polygon.area)
for inverse_velocity, time in polygon.vertices:
print(f' {inverse_velocity:.6e} s/m at {time * 1e3:.4f} ms')
Each Polygon has vertices, area and contains. The vertices are the disc edges, at
the precision of a double.
Because the pieces are disjoint — two turns of one disc cannot both pass the same
neutron — region.area is a plain sum, with no inclusion-exclusion.
Ask about one neutron¶
centre = region.polygons[0].vertices[0]
print(region.contains(*centre)) # a vertex is on the boundary, so inside
print(region.contains(5e-3, 1e-3)) # far outside
The boundary counts as inside.
Reuse the source rectangle¶
transmit answers a question; it does not consume the region it was asked of.
# `transmit` answers a question; it does not consume the rectangle
without_bandwidth = source.transmit(train[:4])
print(len(source), len(region), len(without_bandwidth))
So one rectangle can be transmitted through several trains, or through prefixes of one, to see which discs are doing the cutting.
Write it out¶
data = region.to_dict(sampled=source)
print(sorted(data))
print(data['acceptance'])
# region.write_json('region.json', sampled=source) # the same thing, on disk
region.to_dict(sampled=source) gives plain data; region.write_json(path, sampled=source)
writes the same thing to a file — byte for byte what the Polygon_ESS_butterfly McStas
component writes beside its data. Every number carries enough digits to read back
bit-exact, and the file is a few hundred bytes where a mask of any useful resolution is
megabytes.
sampled is what sets the acceptance; without it both it and acceptance are null.
Next¶
- Sample without wasting rays — what the region is for.
- Allow for a guide's path spread.