Geometry#
A geometry file describes the physical detectors used to convert fitted pixel positions into scattering-vector directions. Load it once when several frames share the same detector configuration.
Load geometry#
load_geometry() parses and validates detector geometry:
from lauelab.indexing import load_geometry
geometry = load_geometry("geometry.xml")
print(geometry.detector_count)
You can pass the returned Geometry directly to Indexer. This avoids parsing the file again when you construct related indexers.
from lauelab.indexing import Indexer
indexer = Indexer(geometry, "crystal.xml")
Select a detector#
Select a detector by physical slot or exact detector identifier:
slot = geometry.find_detector("PE1621 723-3335")
if slot < 0:
raise ValueError("detector is not present in the geometry")
indexer = Indexer(geometry, "crystal.xml", detector_index=slot)
Indexer(..., detector_id="PE1621 723-3335") performs the lookup and reports an InputError if the identifier is absent. If both selection arguments are supplied, detector_id determines the selected slot.
Warning
A detector index is a physical slot from the geometry file. It is not a zero-based position in the list of active detectors. If slots 0 and 2 are active, detector_index=1 is invalid rather than a reference to slot 2.
Pixel coordinates and grouping#
Frame coordinates use zero-based (x, y) order. NumPy arrays use [y, x] indexing and have shape (ny, nx).
start=(start_x, start_y) gives the full-detector origin of an in-memory frame. group=(group_x, group_y) gives the positive integer detector-pixel grouping factor. The complete grouped frame must fit inside the selected detector. See Pixel-to-q conversion for the coordinate equations and physical transformation.
Inspect detector metadata#
Geometry.detector(slot) returns immutable metadata for one active slot:
detector = geometry.detector(slot)
print(detector.detector_id)
print(detector.nx, detector.ny)
print(detector.size_x, detector.size_y)
nx and ny are full-detector dimensions in unbinned pixels. size_x, size_y, and translation use micrometres. rotation_vector is an axis-angle vector in radians, and rotation is the corresponding (3, 3) matrix.
detector_count reports the number of active detectors. It does not identify their slots.
Inspect wire metadata#
Geometry.wire returns immutable WireGeometry metadata when the geometry file has a complete wire section. It returns None for a detector-only geometry. The diameter, focal distance, and origin use µm; the rotation magnitude uses degrees.
wire = geometry.wire
if wire is not None:
print(wire.diameter_um, wire.origin_um)
Reconstruction requires complete wire geometry and raises InputError when it is absent. Indexing continues to accept detector-only geometry files.
Validation errors#
Geometry loading raises ValueError for malformed, incomplete, duplicated, or physically invalid detector declarations. Detector lookup returns -1 for an unknown identifier. Geometry.detector() raises ValueError for an inactive or out-of-range slot. Indexer converts detector-selection failures to InputError.
Do not continue with a different detector after a selection failure. Detector geometry determines every qhat value and therefore affects orientation indexing.
Convert known pixel positions#
Use Geometry.pixels_to_q() when peak positions already exist and you only need geometry conversion:
import numpy as np
peaks_xy = np.array([
[720.25, 512.75],
[1311.50, 984.00],
])
qhat = geometry.pixels_to_q(
peaks_xy,
detector_index=slot,
start=(0, 0),
group=(1, 1),
)
assert qhat.shape == (2, 3)
Each row of qhat is a unit scattering vector in the 34-ID-E laboratory convention.