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.