Calibration#

lauelab.calibration fits the rotation and translation of a detector to reflections of a crystal with a known lattice, and writes the result into a copy of a geometry file. The guide contains a complete synthetic workflow. The algorithm page defines the error that the fit minimizes and its weights.

The module consists of functions and has no configured object. A calibration is made a few times a year from one frame, so there is no configuration to reuse between calls. Each function returns a new value and keeps no state.

lauelab.calibration.calibration_table(peaks, pattern, *, start=(0, 0), group=(1, 1), previous=None)[source]#

Return the calibration table of the peaks that a pattern indexed.

This replaces LaueGo’s MakeCalibData, which matched each fitted peak to the nearest indexed peak. A Pattern records the peak of each reflection in pk_index, so no position matching is needed.

Parameters:
  • peaks – The peaks whose scattering vectors were indexed, in the same order: frame (x, y) coordinates with shape (n, 2), or a structured array with fit_x and fit_y fields such as PeakSearch.peaks.

  • pattern – One pattern returned by index_orientations().

  • start (tuple[int, int]) – Full-detector origin and pixel grouping of the frame, as (x, y).

  • group (tuple[int, int]) – Full-detector origin and pixel grouping of the frame, as (x, y).

  • previous (CalibrationTable | None) – An earlier table of the same frame. Measured energies, their sources, and Miller indices of rows with an energy are copied to the rows of the same peak (same full-detector position within 0.01 pixel).

Returns:

CalibrationTable – One row per indexed peak, in the order of pattern.pk_index.

Return type:

CalibrationTable

class lauelab.calibration.CalibrationTable(hkl, pixels, energy_kev, energy_source, peak_index)[source]#

Reflections with known Miller indices and their measured values.

Each row is one reflection. A row is used when it has a pixel position, a measured energy, or both, as in LaueGo’s CalibrationList.

Parameters:
  • hkl (numpy.ndarray) – Integer Miller indices with shape (n, 3).

  • pixels (numpy.ndarray) – Zero-based full-detector (x, y) peak positions with shape (n, 2); NaN where the position was not measured.

  • energy_kev (numpy.ndarray) – Measured photon energies in keV with shape (n,); NaN where the energy was not measured.

  • energy_source (tuple[str, ...]) – One free-text note per row, such as the scan in which the energy was measured; an empty string where there is none.

  • peak_index (numpy.ndarray) – Zero-based index of each row’s peak in the peak array that was indexed, with shape (n,); -1 for a row without a peak.

Notes

The arrays are read-only. with_energy() and with_hkl() return changed copies.

property n_energies: int#

Number of rows with a measured energy.

with_energy(row, energy_kev, source='')[source]#

Return a copy with the measured energy of one row set or cleared.

Parameters:
  • row (int) – Zero-based row index.

  • energy_kev (float | None) – Measured energy in keV, or None to clear it.

  • source (str) – Free-text note on where the energy was measured.

with_hkl(row, hkl)[source]#

Return a copy with the Miller indices of one row changed.

Use this to select another harmonic of a reflection whose measured energy belongs to a higher order, such as 008 instead of 004.

lauelab.calibration.calibrate_detector(table, crystal, reciprocal, detector, *, fit_sample_rotation=True, azimuth_deg=90.0, max_nfev=2000)[source]#

Fit a detector’s rotation and translation to reflections of a known crystal.

This is LaueGo’s detector calibration (OptimizeAll in Calibration.ipf). For each row of table, the scattering vector calculated from the crystal and the sample rotation is compared with the measured one: its direction from the pixel position and its length from the measured energy. Peaks with a measured energy have a larger weight, min((n / n_energy)**2, 40).

When fit_sample_rotation is true (detector 0 in LaueGo), the sample rotation is fitted with the detector. A rotation of the sample and the detector together about the incident beam changes no measured value, so an extra term holds the detector azimuth at azimuth_deg, weighted 10 times. When it is false (the other detectors), the sample rotation of reciprocal is fixed; use the refined_reciprocal of a calibration of detector 0.

Parameters:
  • table (CalibrationTable) – Measured reflections, such as the result of calibration_table().

  • crystal (Crystal | str | Path) – The calibration crystal, or the path of its crystal XML file.

  • reciprocal – Reciprocal basis rows in 1/nm, including 2π, of the sample orientation, such as Pattern.reciprocal. Its rotation from the crystal reference basis is the starting sample rotation.

  • detector (DetectorGeometry) – Starting detector geometry, from Geometry.detector.

  • fit_sample_rotation (bool) – Fit the sample rotation with the detector. At least one measured energy is then required, as in LaueGo.

  • azimuth_deg (float) – Detector azimuth about the incident beam in degrees, used only when fit_sample_rotation is true. 90 is the 34-ID-E value of LaueGo’s beamline preference.

  • max_nfev (int) – Maximum number of residual evaluations.

Returns:

DetectorCalibration – The refined detector, sample rotation, per-row errors, and status. A fit that cannot run or does not converge returns a result whose status says why.

Raises:

InputError – If an argument is malformed.

Return type:

DetectorCalibration

class lauelab.calibration.DetectorCalibration(status, message, table, initial, refined, initial_sample_rotation, refined_sample_rotation, fit_sample_rotation, azimuth_deg, rotation_vector_sigma, translation_sigma_um, sample_rotation_sigma, error_start, error_end, n_evaluations, predicted_pixels, pixel_error, angle_error_deg, calculated_energy_kev, energy_error_ev, reference_reciprocal, crystal_name, crystal_source)[source]#

Result of calibrate_detector().

Lengths are in µm, rotation vectors are axis times angle in radians, and rotation matrices are proper, dimensionless (3, 3) arrays. Per-row arrays follow the rows of table.

status#

One of "ok", "insufficient_data", "energy_mismatch" and "not_converged". Only "ok" results have refined values; the others repeat the initial values.

Type:

str

message#

Explanation of the status.

Type:

str

table#

The measured reflections.

Type:

lauelab.calibration._table.CalibrationTable

initial, refined

The detector before and after the fit. refined.rotation_vector and refined.translation are rounded as Igor rounds them: the rotation vector to 1e-8 rad, and the translation to the power of ten nearest 1 % of a pixel.

initial_sample_rotation, refined_sample_rotation

Rotation from the crystal reference basis to the laboratory frame, so that q = rotation @ reference.T @ hkl. They are equal when the sample rotation was not fitted.

fit_sample_rotation#

Whether the sample rotation was fitted.

Type:

bool

azimuth_deg#

Required detector azimuth about the incident beam in degrees, or None when the sample rotation was not fitted.

Type:

float | None

rotation_vector_sigma, translation_sigma_um, sample_rotation_sigma

Standard errors of the detector rotation vector (rad), translation (µm), and sample rotation about the laboratory x, y and z axes (rad), from the fit residuals and Jacobian. They depend on Igor’s relative weights and are not a calibrated measurement uncertainty. NaN when not fitted or not determined.

error_start, error_end

Igor’s error, the root of the weighted mean square of the reflection errors (dimensionless, near radians) combined with the azimuth error, before and after the fit.

n_evaluations#

Number of residual evaluations.

Type:

int

predicted_pixels#

Full-detector (x, y) position of each reflection on the refined detector, with shape (n, 2); NaN off the detector.

Type:

numpy.ndarray

pixel_error#

predicted_pixels - table.pixels with shape (n, 2).

Type:

numpy.ndarray

angle_error_deg#

Angle between the calculated and the measured scattering vector of each row with a pixel, shape (n,).

Type:

numpy.ndarray

calculated_energy_kev#

Energy of each reflection calculated from the refined sample rotation and the crystal, shape (n,), as Igor’s calculated_keV column.

Type:

numpy.ndarray

energy_error_ev#

Measured minus calculated energy in eV, shape (n,).

Type:

numpy.ndarray

reference_reciprocal#

Reciprocal basis of the crystal reference orientation, rows in 1/nm including 2π.

Type:

numpy.ndarray

property success: bool#

Whether the fit produced refined values.

property refined_reciprocal: numpy.ndarray#

Reciprocal basis rows (1/nm, including 2π) of the refined sample orientation.

Pass it to calibrate_detector() to calibrate another detector with the sample rotation fixed.

property sample_rotation_vector: numpy.ndarray#

Refined sample rotation as an axis-angle vector in radians (Igor’s rho).

property rms_pixel_error: float#

Root-mean-square distance in pixels between predicted and measured positions.

property rms_energy_error_ev: float#

Root-mean-square measured minus calculated energy in eV.

summary_note()[source]#

Return a one-line ASCII summary for the <note> of a geometry file.

The text is shorter than 100 characters, the length of Igor’s geoNote field.

provenance()[source]#

Return the fit inputs and settings as a JSON-compatible dictionary.

lauelab.calibration.surface_check(crystal, reciprocal, *, family=(0, 0, 1), expected_normal=(0.0, np.float64(0.7071067811865476), np.float64(-0.7071067811865476)))[source]#

Find where the surface reflection of an oriented crystal points.

The calibration standard at 34-ID-E is assumed to be a Si wafer with a (001) surface whose normal is 45 degrees between the reversed incident beam and the +y axis. Its surface reflection, such as 004, then lies between the incident beam and the beam diffracted toward the detector. A sample rotation applied in the wrong direction (the transpose of the rotation) moves it elsewhere.

Parameters:
  • crystal (Crystal | str | Path) – The calibration crystal, or the path of its crystal XML file.

  • reciprocal – Reciprocal basis rows in 1/nm, including 2π, of the oriented crystal, such as Pattern.reciprocal or DetectorCalibration.refined_reciprocal.

  • family – Miller indices of the surface reflection. For a cubic crystal every signed permutation is a member of the family; otherwise family and its negative are.

  • expected_normal – Expected direction of the surface normal in the 34-ID-E laboratory frame. It is normalized.

Returns:

SurfaceCheck – The closest family member and its angles.

Return type:

SurfaceCheck

class lauelab.calibration.SurfaceCheck(hkl, direction, expected_normal, exit_direction, angle_to_expected_deg, angle_to_incident_deg, angle_to_exit_deg, between)[source]#

Direction of the surface reflection of an oriented crystal.

hkl#

The member of the requested family whose scattering vector is closest to the expected surface normal, shape (3,).

Type:

numpy.ndarray

direction#

Its unit scattering vector in the 34-ID-E laboratory frame, shape (3,).

Type:

numpy.ndarray

expected_normal#

The expected surface normal, a unit vector with shape (3,).

Type:

numpy.ndarray

exit_direction#

Direction of the beam that the expected surface would reflect specularly, shape (3,).

Type:

numpy.ndarray

angle_to_expected_deg#

Angle between direction and expected_normal.

Type:

float

angle_to_incident_deg#

Angle between direction and the reversed incident beam (-z).

Type:

float

angle_to_exit_deg#

Angle between direction and exit_direction.

Type:

float

between#

Whether direction is closer to both the reversed incident beam and the exit direction than they are to each other, so that it lies between the two beams.

Type:

bool

Notes

For a cubic crystal some member of a family such as {001} is always within about 55 degrees of any direction, so between can be true for a wrong orientation. angle_to_expected_deg shows the error: for the transpose of Igor’s perfect-Si rotation it is 29.9 degrees, against 1.1 degrees for the rotation itself.

lauelab.calibration.write_geometry(source, destination, detectors, *, note, file_note=None, written=None)[source]#

Write a copy of a geometry file with calibrated detectors.

Only these parts of source change:

  • <R>, <P>, <timeMeasured> and <note> of each detector whose <ID> equals the detector_id of one of detectors;

  • <dateWritten>, <timeWritten> and <EPOCH>;

  • <fileNote>, if file_note is given.

Every other byte, including the other detectors, the wire and the sample, is copied. The C programs and Igor find elements by searching the text for their tags, so no new element names are added.

The file is written to a temporary file in the destination folder, read back with load_geometry() and compared with detectors, and then linked to destination.

Parameters:
  • source (str | Path) – The geometry XML file that the calibration started from.

  • destination (str | Path) – The new file. It must not exist.

  • detectors (Iterable[DetectorGeometry]) – Calibrated detectors, such as the refined detector of a DetectorCalibration.

  • note (str) – Text of each calibrated detector’s <note>: printable ASCII without <, > or &, at most 99 characters.

  • file_note (str | None) – Text of the file’s <fileNote>, with the same characters allowed.

  • written (datetime.datetime | None) – Time recorded as the measurement and writing time. The default is the current local time.

Returns:

pathlib.Path – destination.

Raises:
  • InputError – If source is not a geometry XML file with these detectors and units radian and mm, contains an XML tag inside a comment, or a note is not allowed.

  • FileExistsError – If destination exists.

Return type:

Path

lauelab.calibration.fit_peak(image, x, y, *, half_width=10)[source]#

Fit a two-dimensional Gaussian to the peak nearest a chosen position.

This replaces LaueGo’s “Add Gaussian to pkList”. The fit region is the square of 2 * half_width + 1 pixels centred on the brightest pixel within half_width of (x, y). The model is a constant background plus a Gaussian with independent widths along x and y.

Parameters:
  • image – Two-dimensional frame indexed image[y, x].

  • x (float) – Zero-based frame pixel coordinates of the chosen position.

  • y (float) – Zero-based frame pixel coordinates of the chosen position.

  • half_width (int) – Half-width in pixels of the search and fit regions, at least 3.

Returns:

numpy.ndarray or None – One peak with shape (1,) and the fields of PeakSearch.peaks, so it can be appended to them with numpy.concatenate(), or None if no peak can be fitted: the fit fails, the amplitude is not positive, or the centre is not inside the fit region. intens is the Gaussian amplitude above background, integral its integral, hwhm_x and hwhm_y its half widths at half maximum in pixels, tilt is zero, and chisq is the mean square fit residual.

Return type:

np.ndarray | None

lauelab.calibration.peak_directions(detector, pixels)[source]#

Return unit scattering vectors of full-detector pixels for a source at the origin.

Parameters:
  • detector (DetectorGeometry) – Detector geometry.

  • pixels – Zero-based full-detector (x, y) coordinates with shape (n, 2).

Returns:

numpy.ndarray – Unit scattering vectors in the 34-ID-E laboratory frame with shape (n, 3). For a sample at depth zero these are the vectors that pixels_to_q() returns.

Return type:

numpy.ndarray

lauelab.calibration.detector_azimuth_deg(detector)[source]#

Return the azimuth of a detector about the incident beam in degrees.

This is Igor’s AzimuthAngle. For a detector far from the incident beam it is the azimuth of the detector’s z axis; for a detector close to forward or back scattering it is the azimuth of its +x pixel direction.

lauelab.calibration.rotation_matrix(rotation_vector)[source]#

Return the rotation matrix of an axis-angle rotation vector in radians.

lauelab.calibration.rotation_vector(matrix)[source]#

Return the axis-angle rotation vector in radians of a rotation matrix.

The angle is in [0, pi], as in Igor’s axisOfMatrix.

lauelab.calibration.matrix_from_igor(text)[source]#

Read a 3 x 3 matrix that Igor wrote in a wave note.

Igor writes a matrix such as rotation_matrix0 or recip_lattice0 as {{m00,m10,m20}{m01,m11,m21}{m02,m12,m22}}: each inner group is one column. Reading the groups as rows gives the transpose, which for a rotation is the inverse rotation.

Parameters:

text (str) – The matrix text, with or without spaces and commas between groups.

Returns:

numpy.ndarray – The matrix with shape (3, 3), so that matrix[i, j] is Igor’s m[i][j].

Raises:

InputError – If the text does not contain three groups of three numbers.

Return type:

numpy.ndarray

lauelab.calibration.HC_KEV_NM = 1.239841984#

Convert a string or number to a floating-point number, if possible.

lauelab.calibration.DEFAULT_SURFACE_NORMAL = (0.0, np.float64(0.7071067811865476), np.float64(-0.7071067811865476))#

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable’s items.

If the argument is a tuple, the return value is the same object.

lauelab.calibration.STATUSES = ('ok', 'insufficient_data', 'energy_mismatch', 'not_converged')#

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable’s items.

If the argument is a tuple, the return value is the same object.

Units and shapes#

Quantity

Convention

Pixel positions

Arrays of shape (n, 2) in zero-based full-detector (x, y) coordinates. NaN marks a missing value. The peaks argument of calibration_table() and the positions that fit_peak() accepts and returns are frame coordinates.

Translations

µm

Energies

keV

Energy errors

eV

Angles

Degrees. Rotation vectors, the standard errors rotation_vector_sigma and sample_rotation_sigma, and values whose name ends in _rad are in radians.

Rotation vectors

Axis times angle in radians

Reciprocal matrices

Rows a*, b*, c* in 1/nm, including \(2\pi\)

Ownership and errors#

The arrays of a CalibrationTable, a DetectorCalibration and a SurfaceCheck are read-only.

Invalid arguments raise InputError. write_geometry() raises FileExistsError when the destination exists.

A fit that cannot run or does not converge raises no exception. calibrate_detector() returns a DetectorCalibration whose status is not "ok", and the refined values are equal to the initial values.